Context managers are Python’s answer to resource management — ensuring that files get closed, locks get released, and database connections get returned to the pool, even when exceptions occur. The with statement makes this pattern concise and reliable.
The with Statement#
The most common context manager is open() for file handling:
with open("example.txt", "w") as file:
file.write("Hello, World!")
# File is automatically closed here, even if write() raises an exception
Without with, you’d need a try/finally block:
file = open("example.txt", "w")
try:
file.write("Hello, World!")
finally:
file.close()
The with statement is cleaner, safer, and impossible to forget.
How Context Managers Work#
A context manager is any object that implements two dunder methods:
__enter__()— called when entering thewithblock. Its return value is bound to theasvariable.__exit__(exc_type, exc_val, exc_tb)— called when leaving thewithblock, whether normally or via an exception.
class ManagedFile:
def __init__(self, filename, mode):
self.filename = filename
self.mode = mode
def __enter__(self):
self.file = open(self.filename, self.mode)
return self.file
def __exit__(self, exc_type, exc_val, exc_tb):
self.file.close()
return False # Don't suppress exceptions
with ManagedFile("test.txt", "w") as f:
f.write("Custom context manager!")
If __exit__ returns True, the exception is suppressed. Returning False (or None) lets it propagate — which is almost always what you want.
The contextlib Shortcut#
Writing a full class for every context manager is verbose. The contextlib.contextmanager decorator lets you use a generator instead:
from contextlib import contextmanager
@contextmanager
def managed_file(filename, mode):
file = open(filename, mode)
try:
yield file
finally:
file.close()
with managed_file("test.txt", "w") as f:
f.write("Generator-based context manager!")
Everything before yield is the setup (__enter__), and everything after is the teardown (__exit__). The try/finally ensures cleanup happens even if the body raises.
Practical Examples#
Database Connections#
import sqlite3
with sqlite3.connect('mydatabase.db') as conn:
cursor = conn.cursor()
cursor.execute('SELECT * FROM users')
results = cursor.fetchall()
# Connection is automatically closed
Timing Code#
import time
from contextlib import contextmanager
@contextmanager
def timer(label):
start = time.perf_counter()
yield
elapsed = time.perf_counter() - start
print(f"{label}: {elapsed:.4f} seconds")
with timer("Data processing"):
total = sum(range(10_000_000))
# Output: Data processing: 0.1842 seconds
Thread Locks#
import threading
lock = threading.Lock()
with lock:
# Critical section — only one thread at a time
shared_resource.update(new_data)
# Lock is automatically released
Temporary Directory#
import tempfile
import os
with tempfile.TemporaryDirectory() as tmpdir:
filepath = os.path.join(tmpdir, "data.txt")
with open(filepath, "w") as f:
f.write("temporary data")
# Do work with the temp directory
# Directory and all contents are deleted automatically
Suppressing Exceptions#
from contextlib import suppress
with suppress(FileNotFoundError):
os.remove("file_that_might_not_exist.txt")
# No crash if the file doesn't exist
Multiple Context Managers#
Python 3.10+ supports using parentheses for cleaner multi-line with statements:
with (
open("input.txt") as infile,
open("output.txt", "w") as outfile,
):
for line in infile:
outfile.write(line.upper())
When to Use Context Managers#
Use them whenever you have setup/teardown pairs:
- Opening and closing files
- Acquiring and releasing locks
- Connecting and disconnecting from databases
- Starting and stopping timers
- Creating and cleaning up temporary resources
- Changing and restoring state (e.g., working directory, environment variables)
The pattern ensures cleanup happens reliably, which eliminates an entire category of resource leak bugs.
See also:
- Python Magic Methods — the
__enter__and__exit__dunder methods that power context managers - Getting Started with Requests — the Requests library uses context managers for session and response handling