Mastering Python Context Managers for Clean Resource Handling
Why context managers matter
Every Python developer has felt the pain of a file left open, a database connection that never returns to the pool, or a timer that keeps running after the block finishes. The with statement solves exactly this by guaranteeing that cleanup code runs, even when an exception bubbles up. I reach for a custom context manager whenever I see a setup‑/teardown pair repeated across the codebase — it turns a fragile pattern into a single, reusable abstraction.
Building a reusable timer
Profiling a hot path is a classic use case. Instead of sprinkling time.perf_counter() calls everywhere, wrap the region in a timer that logs the elapsed milliseconds automatically.
import time
import logging
from contextlib import contextmanager
@contextmanager
def timed_block(name: str):
"""Log how long the wrapped block takes."""
start = time.perf_counter()
try:
yield
finally:
elapsed = (time.perf_counter() - start) * 1000
logging.info(f'{name} took {elapsed:.2f} ms')
# Usage
with timed_block('payload‑serialization'):
data = serialize(payload)
send(data)
The finally clause guarantees the log line appears even if serialize raises. Because the helper is a generator decorated with @contextmanager, the boilerplate of a full class disappears while the semantics stay identical.
Real‑world: database transaction wrapper
In a service that talks to PostgreSQL, every write operation should either commit cleanly or roll back on error. A context manager makes the contract explicit and prevents the dreaded "forgot to commit" bug.
from contextlib import contextmanager
import psycopg2
@contextmanager
def transaction(conn):
"""Yield a cursor that commits on success, rolls back on failure."""
cur = conn.cursor()
try:
yield cur
conn.commit()
except Exception:
conn.rollback()
raise
finally:
cur.close()
# In a request handler
with transaction(db_conn) as cur:
cur.execute('INSERT INTO events (type, payload) VALUES (%s, %s)',
(event_type, json.dumps(payload)))
Notice the raise after rollback — it preserves the original traceback for upstream error handling. The caller never sees a raw cursor; they only interact with the logical unit of work.
When to prefer a class‑based manager
Generator‑based managers are concise, but a class gives you explicit __enter__ and __exit__ methods, which is handy when you need to store state across the block or expose a richer API.
class TempDirectory:
def __init__(self, prefix='tmp_'):
self.prefix = prefix
self.path = None
def __enter__(self):
import tempfile, os
self.path = tempfile.mkdtemp(prefix=self.prefix)
return self.path
def __exit__(self, exc_type, exc, tb):
import shutil
if self.path and os.path.exists(self.path):
shutil.rmtree(self.path, ignore_errors=True)
# Return False to propagate any exception
return False
# Usage
with TempDirectory('build_') as workdir:
compile_sources(workdir)
run_tests(workdir)
The class cleanly isolates the temporary directory lifecycle and can be extended — for example, adding a list_files() helper — without cluttering the calling code.
Common pitfalls and how to avoid them
- Swallowing exceptions: Returning
Truefrom__exit__or omittingraisein a generator silences errors. Always let the exception propagate unless you deliberately handle it. - Resource leakage in
__enter__: If__enter__raises,__exit__is never called. Perform only cheap, non‑resource‑acquiring work there; acquire heavy resources inside thetryblock of a generator or after theyield. - Re‑entrancy: A manager instance shouldn't be reused across nested
withstatements unless you explicitly design it for that. Create a fresh instance each time.
Putting it all together
Context managers are the Pythonic way to express "do this, then guarantee that". Whether you need a quick timer, a transaction guard, or a temporary workspace, the pattern reduces boilerplate, centralizes cleanup, and makes the intent obvious to the next reader — often yourself six months later. Start by identifying every try/finally pair in your codebase; each is a candidate for a tiny, well‑named manager. The investment pays off in fewer bugs, clearer logs, and a codebase that feels more maintainable.