Chapter 10: Files and Input/Output
Programs often need to exchange information with the outside world, such as reading data, saving results, or communicating through text streams. Python’s input and output (I/O) system makes this process simple and expressive. Whether you are reading a configuration file, writing logs, or processing large datasets, Python provides consistent interfaces that work across platforms and file types.
The core concept is the file object. A file object represents an open connection to a file or stream, allowing you to read, write, or both. You usually open a file with the built-in open() function, which returns a handle that you can use within a with statement for automatic cleanup. Once open, you can operate on the file using methods like read(), write(), or readlines().
with open("example.txt", "w", encoding="utf-8") as f:
f.write("Hello, world!\n")
This pattern ensures the file is properly closed, even if an error occurs. It is part of Python’s design philosophy that explicit resource management should be easy and safe.
Beyond ordinary text files, Python’s I/O system extends to binary data, temporary files, compressed archives, network streams, and in-memory buffers. The standard library offers modules for each of these, from io and pathlib to gzip and zipfile. Working with files becomes a matter of combining these tools cleanly and predictably.
In this chapter you will learn how to open, read, and write files safely using with blocks, the differences between text and binary modes, how to work with file paths and directories using pathlib, how to handle exceptions and detect file errors, and how to use modules for structured data such as CSV, JSON, and binary formats
Reading and writing files
File I/O in Python revolves around the built-in open() function. It takes a filename, a mode string that determines how the file will be used, and optional parameters like the character encoding. Once opened, the returned file object provides methods for reading and writing data.
Opening a file
f = open("notes.txt", "r", encoding="utf-8")
content = f.read()
f.close()
Although you can open and close files manually, the preferred and safest pattern is to use a with statement. This automatically closes the file even if an error occurs.
with open("notes.txt", "r", encoding="utf-8") as f:
content = f.read()
print(content)
with open(...) when dealing with files. It prevents resource leaks and ensures files are closed cleanly.
Writing text files
To create or overwrite a file, use mode "w". To append to an existing file, use "a". In both cases, open the file in text mode and specify the encoding explicitly.
with open("output.txt", "w", encoding="utf-8") as f:
f.write("First line\n")
f.write("Second line\n")
with open("output.txt", "a", encoding="utf-8") as f:
f.write("Appended line\n")
You can also write multiple lines at once using writelines() if you already have a list of strings.
lines = ["alpha\n", "beta\n", "gamma\n"]
with open("greek.txt", "w", encoding="utf-8") as f:
f.writelines(lines)
Reading files efficiently
When reading, you can process entire contents at once with read(), a fixed number of bytes with read(n), or line by line with readline() or iteration.
with open("notes.txt", "r", encoding="utf-8") as f:
for line in f:
print(line.strip())
Reading line by line is memory efficient and suitable for large files. Each iteration reads only the next line from the stream.
Binary files
When working with non-text data (for example, images, sound files, or compiled programs), open the file in binary mode by adding "b" to the mode string. In this mode, read and write operations deal with bytes objects rather than strings.
# Copy a binary file safely
with open("photo.jpg", "rb") as source, open("backup.jpg", "wb") as target:
target.write(source.read())
Modes summary
| Mode | Meaning |
"r" | Read text (default) |
"w" | Write text, truncating the file if it exists |
"a" | Append text to the end |
"rb" | Read binary data |
"wb" | Write binary data |
"ab" | Append binary data |
"r+" | Read and write (must already exist) |
"w+" | Create or overwrite, then read and write |
File positions and seeking
You can move the current position in an open file using seek() and find out where you are with tell().
with open("data.bin", "rb") as f:
f.seek(10) # skip first 10 bytes
chunk = f.read(4) # read next 4 bytes
print(f.tell()) # current position
These same patterns apply whether you are dealing with small text files or multi-gigabyte binary data: open safely, process in chunks, and close automatically.
Context managers
Context managers make resource handling predictable, elegant, and safe. They are one of Python’s most practical features for writing reliable code that always cleans up after itself.
Python’s with statement is the standard way to manage resources that need to be set up and then cleaned up automatically. It ensures that even if an error occurs inside the block, the resource is released properly. Files are the most common example, but the same pattern works for network connections, database sessions, and locks.
with open("log.txt", "w", encoding="utf-8") as f:
f.write("Session started\n")
f.write("All systems nominal\n")
When the with block ends, Python calls the file object’s __exit__() method, which closes the file automatically. This avoids subtle bugs such as leaving files open or forgetting to release system resources.
with statement is not limited to files. Any object that defines __enter__() and __exit__() methods can be used as a context manager.
How it works
When you write:
with open("example.txt", "r", encoding="utf-8") as f:
data = f.read()
Python performs the following steps:
- Calls
open()to get a file object. - Invokes the object’s
__enter__()method and assigns its result tof. - Executes the block of code inside the
withstatement. - When the block finishes (even if an exception is raised), calls the object’s
__exit__()method to handle cleanup.
Creating your own context manager
You can define custom context managers by writing a class that implements __enter__ and __exit__. This is useful when you want to ensure something is always released or reset.
class ManagedResource:
def __enter__(self):
print("Resource acquired")
return self
def __exit__(self, exc_type, exc_val, exc_tb):
print("Resource released")
with ManagedResource() as r:
print("Using resource")
Output:
Resource acquired
Using resource
Resource released
If an exception occurs inside the block, Python passes it to __exit__() as arguments (exc_type, exc_val, and exc_tb). If __exit__() returns True, the exception is suppressed; otherwise it propagates normally.
@contextmanager can simplify setup and teardown logic while keeping the function concise. Use this pattern when you want to manage a temporary resource without defining a full class.
Using the contextlib module
For lightweight cases, you can create context managers using the contextlib module’s @contextmanager decorator. This turns a generator function into a context manager with minimal code.
from contextlib import contextmanager
@contextmanager
def simple_logger(filename):
f = open(filename, "a", encoding="utf-8")
try:
yield f
finally:
f.close()
with simple_logger("events.log") as log:
log.write("Event recorded\n")
Path handling with pathlib
The pathlib module provides an object-oriented interface for working with file system paths. It replaces many of the older os.path and glob functions with cleaner, more readable syntax. A Path object represents a file or directory, and you can use it to inspect, combine, read, and write paths safely across platforms.
from pathlib import Path
p = Path("data") / "records.txt"
print(p) # data/records.txt
print(p.exists()) # True or False
print(p.parent) # data
Unlike string manipulation, pathlib automatically uses the correct separators for each operating system. It also provides intuitive operators for joining paths and accessing metadata.
Creating and inspecting paths
p = Path("/home/robin/documents/report.txt")
print(p.name) # report.txt
print(p.stem) # report
print(p.suffix) # .txt
print(p.parent) # /home/robin/documents
print(p.anchor) # / (root)
Path objects are immutable. Operations such as p / "subdir" or p.with_suffix(".csv") return new paths without altering the original.
Creating directories and files
from pathlib import Path
folder = Path("reports/2025")
folder.mkdir(parents=True, exist_ok=True)
file = folder / "summary.txt"
file.write_text("Quarterly summary pending", encoding="utf-8")
The mkdir() method creates directories, and write_text() writes a full text file in one call. The matching read_text() method reads a file entirely into a string.
content = file.read_text(encoding="utf-8")
print(content)
Iterating through directories
You can loop through directory contents easily using iterdir() or pattern matching with glob() and rglob().
for path in Path("logs").glob("*.txt"):
print(path.name)
# Recursive search
for path in Path("src").rglob("*.py"):
print(path)
rglob() searches recursively through all subdirectories. Use it carefully on large trees, as it can traverse thousands of files.
Combining with file I/O
Each Path object can open files directly, giving a natural link between path handling and file reading or writing.
with Path("output/data.txt").open("w", encoding="utf-8") as f:
f.write("Example output\n")
This is equivalent to open(str(path)) but more idiomatic when you already have a Path object.
Checking file types and metadata
p = Path("photo.jpg")
print(p.exists()) # Does the path exist?
print(p.is_file()) # Is it a file?
print(p.is_dir()) # Is it a directory?
print(p.stat().st_size) # File size in bytes
The stat() method returns system-level information such as size, timestamps, and permissions. You can also resolve paths to absolute locations and check relationships between them:
print(p.resolve()) # Absolute path
print(p.is_relative_to("photos")) # Python 3.9+
Cross-platform paths
pathlib provides PurePath, PurePosixPath, and PureWindowsPath classes for manipulating paths without touching the file system, which is useful when writing tools that must handle multiple platforms.
from pathlib import PureWindowsPath
p = PureWindowsPath("C:/Users/Robin/Documents/file.txt")
print(p.parts)
# ('C:\\', 'Users', 'Robin', 'Documents', 'file.txt')
pathlib for all modern Python code. It integrates smoothly with os, shutil, and file I/O, and eliminates most string-based path bugs.
pathlib brings clarity and consistency to filesystem operations. By treating paths as structured objects rather than raw strings, you write code that is both safer and more expressive across every platform Python supports.
Simple console I/O
Console input and output are the simplest forms of interaction between a Python program and its user. You can display information with print() and collect user input with input(). Both functions handle text automatically and integrate cleanly with Python’s string formatting tools.
Printing output
The print() function writes text to standard output (usually the terminal). It automatically converts its arguments to strings and separates them with spaces by default.
print("Hello", "world") # Hello world
print("Answer:", 42) # Answer: 42
You can control the separator and the line ending using keyword arguments:
print("apple", "banana", "cherry", sep=", ")
# apple, banana, cherry
print("No newline here", end="")
# end="" prevents line break
print() function writes to sys.stdout by default, but you can redirect output to any writable file or stream using the file= argument.
with open("log.txt", "w", encoding="utf-8") as f:
print("Logging started", file=f)
By default, print() flushes its output when a newline is written or when the program ends. For immediate output (useful in progress displays or logging), pass flush=True.
print("Processing...", flush=True)
Reading input
The built-in input() function pauses execution and waits for the user to type a line of text, returning it as a string.
name = input("Enter your name: ")
print(f"Hello, {name}!")
input() is always a string. If you need numeric data, convert it explicitly using int() or float().
age = int(input("Enter your age: "))
print("Next year you will be", age + 1)
Redirecting and scripting
Both input() and print() work seamlessly in scripts or interactive sessions. When reading from redirected input (for example, when piping a file into a program), input() will read lines from that stream instead of a physical keyboard.
# data.txt contains:
# Alice
# Bob
# Charlie
for line in open("data.txt", encoding="utf-8"):
print("Name:", line.strip())
Formatting output neatly
You can combine print() with f-strings or format specifications for aligned or formatted text.
for x in range(1, 6):
print(f"{x:2d} squared is {x*x:3d}")
item = "Apples"
price = 1.49
print(f"{item:<10} £{price:>5.2f}")
# Apples £ 1.49
format() method, f-string formatting syntax, or libraries like textwrap and rich for styled console output.
Working with JSON and CSV
Many programs need to store or exchange structured data. Two of the most common text formats for this are JSON (JavaScript Object Notation) and CSV (Comma-Separated Values). Python’s standard library provides dedicated modules (json and csv) that make it easy to read and write these formats safely and portably.
JSON (JavaScript Object Notation)
JSON represents data as nested structures of dictionaries, lists, strings, numbers, booleans, and null (represented as None in Python). It is widely used for configuration files, APIs, and data exchange between programs.
import json
from pathlib import Path
# Python data structure
user = {
"name": "Ada Lovelace",
"skills": ["mathematics", "programming"],
"active": True,
"projects": 5
}
# Write JSON file
path = Path("user.json")
path.write_text(json.dumps(user, indent=2), encoding="utf-8")
# Read JSON file back
loaded = json.loads(path.read_text(encoding="utf-8"))
print(loaded["name"])
The json module provides four key functions:
json.dump(obj, file)write to an open filejson.load(file)read from an open filejson.dumps(obj)return a JSON stringjson.loads(string)parse a JSON string
# Equivalent file-based approach
with open("user.json", "w", encoding="utf-8") as f:
json.dump(user, f, indent=2)
with open("user.json", encoding="utf-8") as f:
loaded = json.load(f)
indent for readability and sort_keys=True when you need predictable key order. This is useful for diffing or version-controlling JSON files.
CSV (Comma-Separated Values)
CSV files store tabular data where each line represents a row and columns are separated by commas (or sometimes other delimiters such as semicolons or tabs). The csv module provides reader and writer objects that handle quoting and escaping correctly, so you don’t have to parse manually.
import csv
from pathlib import Path
records = [
{"name": "Alice", "age": 30, "city": "London"},
{"name": "Bob", "age": 25, "city": "Paris"},
{"name": "Charlie", "age": 35, "city": "New York"},
]
# Write CSV
with open("people.csv", "w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=["name", "age", "city"])
writer.writeheader()
writer.writerows(records)
# Read CSV
with open("people.csv", newline="", encoding="utf-8") as f:
reader = csv.DictReader(f)
for row in reader:
print(row["name"], "is", row["age"], "years old")
The csv module automatically handles quoting around values that contain commas or newlines. When using DictWriter and DictReader, each row is mapped cleanly to and from a dictionary, making your code clearer and more reliable.
newline="" on Windows. This prevents double line breaks caused by automatic newline translation.