Context Managers as a Pattern
Context managers guarantee setup and teardown around a block of code. The with statement calls __enter__/__exit__ (or @contextmanager yield) so resources release even when exceptions fire - files close, locks release, and transactions roll back.
Recipe
Quick-reference recipe card - copy-paste ready.
from contextlib import contextmanager
@contextmanager
def temporary_setting(obj, attr, value):
old = getattr(obj, attr)
setattr(obj, attr, value)
try:
yield obj
finally:
setattr(obj, attr, old)
with temporary_setting(config, "debug", True):
run_checks()When to reach for this:
- Acquire/release pairs - files, sockets, locks, DB connections
- Temporary state - env vars, log levels, feature flags for a block
- Transactions - commit on success, rollback on exception
- Timing and profiling - enter starts timer, exit logs duration
- Suppressing expected errors -
contextlib.suppress(FileNotFoundError)
Working Example
A class-based manager for database transactions plus a @contextmanager for timing.
import time
from contextlib import contextmanager
from dataclasses import dataclass, field
from typing import Iterator
@dataclass
class FakeConnection:
committed: bool = False
rolled_back: bool = False
def commit(self) -> None:
self.committed = True
def rollback(self) -> None:
self.rolled_back = True
class Transaction:
def __init__(self, conn: FakeConnection) -> None:
self._conn = conn
def __enter__(self) -> FakeConnection:
return self._conn
def __exit__(self, exc_type, exc, tb) -> bool:
if exc_type is None:
self._conn.commit()
else:
self._conn.rollback()
return False # do not suppress exceptions
@contextmanager
def timer(label: str) -> Iterator[None]:
start = time.perf_counter()
try:
yield
finally:
elapsed = time.perf_counter() - start
print(f"{label}: {elapsed:.3f}s")
conn = FakeConnection()
with timer("happy path"):
with Transaction(conn) as db:
print("write rows", db)
print("committed:", conn.committed)
conn2 = FakeConnection()
try:
with Transaction(conn2):
raise ValueError("boom")
except ValueError:
pass
print("rolled back:", conn2.rolled_back)What this demonstrates:
- Class protocol:
__enter__returns the resource,__exit__cleans up - Returning
Falsefrom__exit__propagates exceptions after rollback @contextmanagersplits setup (beforeyield) and teardown (finally)finallyruns whether the block exits normally or via exception
Deep Dive
How It Works
with expr as var:evaluatesexpr, calls__enter__, bindsvar, runs the suite- On exit,
__exit__(exc_type, exc_val, tb)always runs - If
__exit__returns a true value, the exception is suppressed @contextmanagerwraps a generator: code beforeyieldis enter, after is exitcontextlib.ExitStackcomposes multiple managers without deep nesting
Common Manager Types
| Pattern | Enter | Exit |
|---|---|---|
| File I/O | open handle | close handle |
| Lock | acquire | release |
| Transaction | begin | commit or rollback |
| Temp directory | create path | delete tree |
Python Notes
from contextlib import ExitStack, suppress
with ExitStack() as stack:
f1 = stack.enter_context(open("a.txt"))
f2 = stack.enter_context(open("b.txt"))
# both close on exit
with suppress(FileNotFoundError):
open("optional.cfg").read()Gotchas
- Forgetting
finallyin generators - teardown must be infinallyaroundyield, not after it only on success. Fix: always wrap post-yield code intry/finally. - Swallowing exceptions accidentally -
__exit__returningTruehides bugs. Fix: returnFalseunless suppression is intentional (document it). - Re-entrant locks - not all managers are re-entrant; nesting the same lock wrong deadlocks. Fix: use
threading.RLockor document non-reentrancy. - Yielding only once -
@contextmanagermust yield exactly one value. Fix: do not yield in a loop. - Resource leak on
__enter__failure - if setup partially succeeds then raises, exit may not run. Fix: useExitStackor keep setup minimal in__enter__.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
try/finally manual | One-off, no reuse needed | Pattern repeats across codebase |
atexit handlers | Process shutdown cleanup | Per-request or per-scope cleanup |
weakref.finalize | GC-driven cleanup of objects | Deterministic timing required |
| Decorators | Wrapping entire functions | Arbitrary inline blocks need with |
FAQs
What is the difference between a class manager and @contextmanager?
Classes implement __enter__/__exit__. @contextmanager turns a generator with one yield into the same protocol - less boilerplate for simple cases.
Can __exit__ suppress exceptions?
Yes, by returning True. Use sparingly - callers lose visibility into failures unless that is the contract (suppress).
Does with work with async code?
Use async with and __aenter__/__aexit__, or @asynccontextmanager. Sync with cannot await.
What does as var bind to?
The return value of __enter__ (or the value yielded by @contextmanager). Many managers return self or the underlying resource.
How do I compose many managers?
contextlib.ExitStack enters each manager and unwinds in reverse order on exit.
Is open() a context manager?
Yes. with open(path) as f closes the file even if reading raises.
Can I use context managers in dataclass __post_init__?
Prefer callers to use with around construction scope. Long-lived objects should expose .close() or use weakref finalizers.
What about contextlib.closing?
Wraps objects with a .close() method in a context manager when they lack native support.
Should transactions always use context managers?
Yes for application code - commit/rollback pairing is error-prone without guaranteed __exit__.
How do I test context managers?
Assert resource state before, inside, and after with. Trigger exceptions inside the block to verify teardown still runs.
Related
- EAFP vs LBYL - exceptions inside managed blocks
- Factory & Builder Patterns - constructing resources
- Dependency Injection in Python - injecting connections
- Pythonic Patterns Basics - section overview
Stack versions: This page was written for Python 3.14.0 (stable 3.14, maintenance 3.13), FastAPI 0.115+, Django 5.2, Flask 3.1, Pydantic 2, PyTorch 2.6+, pandas 2.2+, Polars 1.x, ruff 0.9+, and uv 0.6+.