Context Managers¶
Use a context manager when entering and leaving a scope must be paired, especially for resources, temporary state, locks, transactions, and instrumentation. The code that acquires a resource normally owns its cleanup unless ownership is explicitly transferred.
Move Lifecycle Policy Out of Business Logic¶
A context manager can remove a large try / except / else / finally frame from a function. The function then reads as the business operation, while the context manager owns setup, rollback, commit, cleanup, and lifecycle-specific exception translation.
Without that extraction, the important work is nested inside resource policy:
def import_orders(connection: Connection, source: Path) -> None:
transaction = connection.begin()
try:
orders = parse_orders(source)
validate_orders(orders)
save_orders(connection, orders)
except Exception:
transaction.rollback()
raise
else:
transaction.commit()
finally:
transaction.close()
Move the reusable lifecycle into a context manager:
from collections.abc import Iterator
from contextlib import contextmanager
@contextmanager
def transaction(connection: Connection) -> Iterator[Connection]:
active_transaction = connection.begin()
try:
yield connection
except Exception as error:
active_transaction.rollback()
raise TransactionError("Database transaction failed") from error
else:
active_transaction.commit()
finally:
active_transaction.close()
The caller is now flat and exposes its purpose immediately:
def import_orders(connection: Connection, source: Path) -> None:
with transaction(connection) as transactional_connection:
orders = parse_orders(source)
validate_orders(orders)
save_orders(transactional_connection, orders)
This extraction is valuable when the context manager represents one coherent lifecycle that is reusable or conceptually separate from the function's work. Do not hide unrelated business branching in a context manager merely to shorten a function.
An exception raised by the with body is delivered to the context manager's exit path. That lets the context manager restore its invariant and add lifecycle context before the exception continues up the stack. It should suppress the exception only when suppression is an explicit part of the contract.
Choose an Implementation¶
| Situation | Prefer |
|---|---|
| An existing object naturally owns the lifecycle | __enter__ / __exit__ or __aenter__ / __aexit__ |
| One small synchronous setup/teardown sequence | @contextmanager |
| One small asynchronous setup/teardown sequence | @asynccontextmanager |
| A runtime-determined set of resources | ExitStack |
| Dynamic sync and async cleanup | AsyncExitStack |
| An optional context manager | nullcontext |
An object with close() but no context protocol |
closing |
Do not wrap an object in closing if it already implements a context manager; its own exit behavior may do more than call close().
Generator Context Managers¶
Code before yield is entry, the yielded value is bound by as, and code after yield is exit. The generator must yield exactly once.
from collections.abc import Iterator
from contextlib import contextmanager
@contextmanager
def transaction(connection: Connection) -> Iterator[Connection]:
try:
yield connection
except Exception:
connection.rollback()
raise
else:
connection.commit()
An exception from the with body is raised at yield. If cleanup catches it only to record or roll back, re-raise it. Returning normally after catching it suppresses the failure.
Use try / finally when cleanup is unconditional:
@contextmanager
def temporary_directory_change(path: Path) -> Iterator[None]:
previous = Path.cwd()
os.chdir(path)
try:
yield
finally:
os.chdir(previous)
Process-global state such as the working directory is unsafe under threads or concurrent tasks. Prefer passing explicit paths; use scoped mutation only when the execution model makes it safe.
Class-Based Protocol¶
Use a class when lifecycle state belongs to the object, multiple methods participate, or a generator would obscure the protocol.
__enter__returns the value bound byas; it need not returnself.__exit__(exc_type, exc_value, traceback)returns a truthy value only to suppress the active exception deliberately.- If acquisition fails partway through
__enter__,__exit__is not called. Clean up partial acquisition inside__enter__or compose acquisition withExitStack. - Context-manager instances are not necessarily reusable or reentrant. Document either property when callers may enter an instance more than once.
Use AbstractContextManager or the ContextManager[T] protocol for annotations when the context behavior, rather than a concrete class, is the contract.
Dynamic Composition¶
ExitStack replaces nested or manually unwound contexts when resources are conditional or data-driven:
from contextlib import ExitStack
with ExitStack() as stack:
files = [stack.enter_context(path.open("rb")) for path in paths]
process(files)
Callbacks and entered contexts unwind in last-in, first-out order. Transfer callbacks with pop_all() only when ownership is intentionally moving to another scope.
AsyncExitStack can combine asynchronous context managers, async cleanup callbacks, and synchronous context managers. Prefer it when one async operation owns a mixed set of resources.
Async Lifecycles¶
Use async with when entry or exit must await, such as opening a client session, acquiring an async lock, or draining a stream.
- Do not perform blocking cleanup in
__aexit__; move blocking work to an appropriate thread or native async API. - Cleanup must still run during cancellation. Catch cancellation only when needed to finish invariant-preserving cleanup, then re-raise it.
- Keep the context open for the complete lazy consumption period. Do not return an iterator or stream whose backing resource has already exited.
- Prefer
aclosingfor async generators that need deterministic cleanup when iteration may end early.
Common Mistakes¶
- Returning a resource created inside
withafter the context has closed it. - Hiding a long-lived resource behind a short function-local context.
- Suppressing exceptions accidentally in
__exit__or a generator context manager. - Mixing resource ownership so both caller and callee attempt cleanup.
- Using a context manager for unrelated before/after actions that do not form one coherent lifecycle.
References: with statement semantics, contextlib, and asynchronous context managers.