Working with Files

Reading from and writing to files in Python.

Introduction

Working with files is essential for many programming tasks. Whether you're reading configuration files, processing data, saving user input, or logging information, Python provides simple and powerful tools for file operations. This lesson covers reading from files, writing to files, and best practices for safe file handling.

Opening Files

Before you can read or write to a file, you need to open it. Python's open() function is used for this purpose.

Basic File Opening

# First create a file to read, so the examples below have something to open
# (writing is covered in detail further down)
with open("data.txt", "w") as file:
    file.write("First line\nSecond line\nThird line\n")

# Open a file for reading
file = open("data.txt", "r")
content = file.read()
file.close()  # Always close files!
print(content)

# Open a file for writing
file = open("output.txt", "w")
file.write("Hello, World!")
file.close()
Expected Output:
First line
Second line
Third line

File Modes

The second argument to open() is the mode. It has two parts: an access mode that says what you intend to do with the file, and optional modifiers that change how the data is handled. If you omit the mode entirely, Python uses "r".

ModeMeaningIf the file already existsIf it does not exist
"r"Read (default)Opens it, positioned at the startFileNotFoundError
"w"WriteErases the contents immediatelyCreates it
"a"AppendKeeps it; writes go to the endCreates it
"x"Exclusive creationFileExistsErrorCreates it

The remaining three characters are modifiers, not modes of their own. You add them to one of the four above:

  • "t" - text mode, the default. Reading gives you a str, and Python translates line endings for you.
  • "b" - binary mode. Reading gives youbytes with no decoding or line-ending translation:"rb", "wb".
  • "+" - adds the missing half of read/write to the access mode, so the file is open for both: "r+" reads and writes without erasing, while "w+" still erases the file first.

Careful: "w" truncates the file the moment you open it, before you write anything. If you meant to add to a file rather than replace it, use "a". When a file must not be overwritten by accident,"x" makes that a hard error instead of silent data loss.

The with Statement (Recommended)

The with statement automatically closes files, even if an error occurs. This is the recommended way to work with files:

# Using 'with' statement (recommended)
with open("data.txt", "r") as file:
    content = file.read()
    # File is automatically closed when block exits
    # Even if an error occurs!
print("Closed automatically:", file.closed)

# Equivalent to:
file = open("data.txt", "r")
try:
    content = file.read()
finally:
    file.close()  # Always executed
print("Closed manually:", file.closed)
Expected Output:
Closed automatically: True
Closed manually: True

Best Practice: Always use the with statement when working with files. It ensures files are properly closed and prevents resource leaks.

Reading Files

Python provides several methods for reading file content, each useful for different scenarios.

Reading Entire File

# Read entire file as string
with open("data.txt", "r") as file:
    content = file.read()
    print(content)

# Read entire file as list of lines
with open("data.txt", "r") as file:
    lines = file.readlines()
    for line in lines:
        print(line.strip())  # strip() removes newline characters
Expected Output:
First line
Second line
Third line

First line
Second line
Third line

Reading Line by Line

For large files, reading line by line is memory-efficient:

# Method 1: Iterate directly over file object
with open("data.txt", "r") as file:
    for line in file:
        print(line.strip())  # Process each line

# Method 2: Using readline() in a loop
with open("data.txt", "r") as file:
    line = file.readline()
    while line:
        print(line.strip())
        line = file.readline()

# Method 3: Read specific number of characters
with open("data.txt", "r") as file:
    chunk = file.read(100)  # Read first 100 characters
    print(chunk)
Expected Output:
First line
Second line
Third line
First line
Second line
Third line
First line
Second line
Third line

Reading with Error Handling

def read_file_safely(filename):
    """Read file content with proper error handling."""
    try:
        with open(filename, "r") as file:
            content = file.read()
            return content
    except FileNotFoundError:
        print(f"Error: File '{filename}' not found.")
        return None
    except PermissionError:
        print(f"Error: Permission denied to read '{filename}'.")
        return None
    except Exception as e:
        print(f"Unexpected error: {e}")
        return None

# Usage
content = read_file_safely("data.txt")
if content:
    print("File read successfully!")
    print(content)
Expected Output:
File read successfully!
First line
Second line
Third line

Writing Files

Writing to files allows you to save data, create logs, and generate output files.

Writing Text

# Write mode ("w") - overwrites existing file
with open("output.txt", "w") as file:
    file.write("Hello, World!\n")
    file.write("This is line 2\n")
    file.write("This is line 3")

# Append mode ("a") - adds to end of file
with open("output.txt", "a") as file:
    file.write("\nThis line is appended")

# Writing multiple lines
lines = ["Line 1\n", "Line 2\n", "Line 3\n"]
with open("output.txt", "w") as file:
    file.writelines(lines)

# Or use write() with join
with open("output.txt", "w") as file:
    file.write("\n".join(["Line 1", "Line 2", "Line 3"]))

# Each "w" wiped the file, so only the last version survives
with open("output.txt", "r") as file:
    print(file.read())
Expected Output:
Line 1
Line 2
Line 3

Write vs Append

Write Mode ("w")
  • Overwrites existing file
  • Creates new file if it doesn't exist
  • Use for creating new files
  • Deletes existing content
Append Mode ("a")
  • Adds to end of existing file
  • Creates new file if it doesn't exist
  • Use for logs or accumulating data
  • Preserves existing content

Writing with Error Handling

def write_file_safely(filename, content):
    """Write content to file with error handling."""
    try:
        with open(filename, "w") as file:
            file.write(content)
        print(f"Successfully wrote to {filename}")
        return True
    except PermissionError:
        print(f"Error: Permission denied to write to '{filename}'.")
        return False
    except Exception as e:
        print(f"Unexpected error: {e}")
        return False

# Usage
success = write_file_safely("output.txt", "Hello, World!")
if success:
    print("File written successfully!")
Expected Output:
Successfully wrote to output.txt
File written successfully!

Working with File Paths

Understanding file paths is important for working with files in different locations.

Relative vs Absolute Paths

import os

# Relative path (relative to current working directory)
with open("data.txt", "r") as file:
    content = file.read()
print("data.txt:", len(content), "characters")

# Relative path with subdirectory
os.makedirs("data", exist_ok=True)
with open("data/input.txt", "w") as file:
    file.write("nested file\n")

with open("data/input.txt", "r") as file:
    content = file.read()
print("data/input.txt:", content.strip())

# Absolute paths start from the root of the filesystem. These are shown as
# syntax only, since they will not exist on your machine:
#
# with open("/home/user/documents/data.txt", "r") as file:
#     content = file.read()
#
# On Windows a backslash starts an escape, so double them (or use a raw string):
#
# with open("C:\\Users\\User\\Documents\\data.txt", "r") as file:
#     content = file.read()
Expected Output:
data.txt: 34 characters
data/input.txt: nested file

Using os.path (Cross-Platform)

The os.path module helps create cross-platform paths:

import os

# Join path components (handles / or \ automatically)
file_path = os.path.join("data", "input", "file.txt")
print(f"Joined path: {file_path}")
# On Linux/Mac: "data/input/file.txt"
# On Windows: "data\input\file.txt"

# Create it so there is something to read
os.makedirs(os.path.join("data", "input"), exist_ok=True)
with open(file_path, "w") as file:
    file.write("nested file\n")

with open(file_path, "r") as file:
    content = file.read()
print(f"Contents: {content.strip()}")

# Check if file exists
if os.path.exists("data.txt"):
    print("File exists!")
else:
    print("File does not exist!")

# The next two depend on where you run the script, so your paths will differ
current_dir = os.getcwd()
print(f"Current directory: {current_dir}")

abs_path = os.path.abspath("data.txt")
print(f"Absolute path: {abs_path}")
Expected Output:
Joined path: data/input/file.txt
Contents: nested file
File exists!
Current directory: /tmp/file-demo
Absolute path: /tmp/file-demo/data.txt

Using pathlib (Modern Approach)

The pathlib module (Python 3.4+) provides an object-oriented approach:

from pathlib import Path

# Create path object
file_path = Path("data") / "input" / "file.txt"
print(file_path)  # Works on all platforms!

# Make sure the folder exists, then write through the Path itself
file_path.parent.mkdir(parents=True, exist_ok=True)
file_path.write_text("Hello, World!")

# Read it back with pathlib's read_text method
content = file_path.read_text()
print(f"Contents: {content}")

# Check if exists
if file_path.exists():
    print("File exists!")

# Get parent directory
parent = file_path.parent
print(f"Parent: {parent}")

# Get filename
filename = file_path.name
print(f"Filename: {filename}")
Expected Output:
data/input/file.txt
Contents: Hello, World!
File exists!
Parent: data/input
Filename: file.txt

Common File Operations

Processing CSV-like Data

# Create a small CSV to work with
with open("data.csv", "w") as file:
    file.write("Alice,30\nBob,25\nCharlie,35\n")

# Reading and processing a simple CSV file
with open("data.csv", "r") as file:
    for line in file:
        # Split by comma (simple CSV parsing)
        fields = line.strip().split(",")
        if len(fields) >= 2:
            name = fields[0]
            age = fields[1]
            print(f"Name: {name}, Age: {age}")

# Writing CSV-like data
data = [
    ["Alice", "30", "Engineer"],
    ["Bob", "25", "Designer"],
    ["Charlie", "35", "Manager"]
]

with open("output.csv", "w") as file:
    for row in data:
        file.write(",".join(row) + "\n")
Expected Output:
Name: Alice, Age: 30
Name: Bob, Age: 25
Name: Charlie, Age: 35

Reading Configuration Files

# Create the configuration file this example reads
with open("config.txt", "w") as file:
    file.write("# App settings\nhost=localhost\nport=8080\ndebug=True\n")

# Simple key=value configuration file
config = {}
with open("config.txt", "r") as file:
    for line in file:
        line = line.strip()
        if line and not line.startswith("#"):  # Skip empty lines and comments
            key, value = line.split("=", 1)
            config[key.strip()] = value.strip()

print(config)
Expected Output:
{'host': 'localhost', 'port': '8080', 'debug': 'True'}

Logging to Files

import datetime

def log_message(message, log_file="app.log"):
    """Append timestamped message to log file."""
    timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    log_entry = f"[{timestamp}] {message}\n"

    with open(log_file, "a") as file:
        file.write(log_entry)

# Usage
log_message("Application started")
log_message("Processing data...")
log_message("Application finished")

# Read the log back. "a" appends, so running this twice doubles the entries.
with open("app.log", "r") as file:
    print(file.read(), end="")
Expected Output:
[2026-09-05 17:25:28] Application started
[2026-09-05 17:25:28] Processing data...
[2026-09-05 17:25:28] Application finished

Binary Files

For binary files (images, executables, etc.), use binary mode:

# Create a small binary file so the examples have something to read.
# These four bytes are the real magic number that starts every JPEG.
with open("image.jpg", "wb") as file:
    file.write(bytes([0xFF, 0xD8, 0xFF, 0xE0]) + b"...not a real image...")

# Reading binary file
with open("image.jpg", "rb") as file:
    binary_data = file.read()
print(f"Read {len(binary_data)} bytes")
print(f"First 4 bytes: {binary_data[:4]}")

# Writing binary file
with open("copy.jpg", "wb") as file:
    file.write(binary_data)

# Copying a file (binary mode)
def copy_file(source, destination):
    with open(source, "rb") as src:
        with open(destination, "wb") as dst:
            dst.write(src.read())

copy_file("image.jpg", "copy2.jpg")

with open("copy2.jpg", "rb") as file:
    print("Copy is identical:", file.read() == binary_data)
Expected Output:
Read 26 bytes
First 4 bytes: b'\xff\xd8\xff\xe0'
Copy is identical: True

Best Practices

✓ Do
  • Always use with statement
  • Handle file errors with try/except
  • Use appropriate file modes
  • Catch FileNotFoundError rather than checking existence first
  • Use pathlib for modern code
  • Close files explicitly if not using with
✗ Don't
  • Forget to close files
  • Ignore file errors
  • Use hardcoded paths
  • Read entire large files into memory
  • Write without error handling
  • Assume file exists

Key Takeaways

Opening Files

  • Use open() function
  • Always use with statement
  • Choose appropriate mode (r, w, a)
  • Files auto-close with with

Reading Files

  • read() - entire file
  • readline() - one line
  • readlines() - all lines
  • Iterate for large files

Writing Files

  • write() - write string
  • writelines() - write list
  • "w" mode overwrites
  • "a" mode appends

File Paths

  • Use os.path for compatibility
  • Use pathlib for modern code
  • Handle relative/absolute paths
  • Check file existence
What's Next?

Great work! You now know how to read from and write to files. This is a fundamental skill for many real-world applications. In future lessons, we'll explore:

  • Modules and packages - Organize and reuse your Python code effectively
  • String manipulation - Master string operations, slicing, and modern formatting
  • List comprehensions - Write cleaner, more Pythonic code with elegant one-liners
  • Standard library essentials - Leverage powerful built-in tools like datetime, json, and collections