Advanced Context Managers
debt(d7/e3/b3/t5)
Closest to 'only careful code review or runtime testing' (d7). pylint/ruff can catch some patterns (e.g. SIM105 for try/except/pass → suppress), but missing context managers around resources like DB connections generally aren't flagged — leaks surface in production or under load testing.
Closest to 'simple parameterised fix' (e3). quick_fix is replacing manual resource management with ExitStack or contextlib.suppress — small localized refactors per usage site, not a single-line swap but well-scoped.
Closest to 'localised tax' (b3). applies_to web/cli; the choice to use (or not use) context managers is generally local to the function handling the resource, not architecturally load-bearing.
Closest to 'notable trap' (t5). The misconception (CMs are only for files) and the @contextmanager 'yield exactly once' rule are documented gotchas most Python devs eventually learn but commonly trip on first.
Also Known As
TL;DR
Explanation
Context managers implement __enter__ and __exit__. contextlib.contextmanager turns a generator function into a context manager — yield once, cleanup after yield runs on exit. contextlib.suppress suppresses specific exceptions. contextlib.ExitStack dynamically composes multiple context managers. contextlib.asynccontextmanager for async with. Use for: database connections, file handles, locks, temporary directories, mocking, and any resource that needs guaranteed cleanup regardless of exceptions.
Common Misconception
Why It Matters
Common Mistakes
- Not using a context manager for database connections — connections leak if an exception occurs before close().
- contextmanager generator that yields more than once — raises RuntimeError; yield exactly once.
- Not using contextlib.suppress for expected exceptions — try/except/pass where suppress is cleaner.
- ExitStack not used when the number of context managers is dynamic — manual nesting becomes unmanageable.
Code Examples
# Resource leak on exception:
def process_file(path):
f = open(path)
data = f.read() # Exception here?
f.close() # Never reached — file handle leaked
return data
from contextlib import contextmanager, suppress
# Generator-based context manager:
@contextmanager
def db_transaction(conn):
try:
yield conn
conn.commit()
except Exception:
conn.rollback()
raise
finally:
conn.close() # Always runs
# Usage:
with db_transaction(get_connection()) as conn:
conn.execute('INSERT INTO orders ...')
# Suppress specific exceptions:
with suppress(FileNotFoundError):
os.unlink('/tmp/lock_file') # OK if already gone