Skip to content

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 by as; it need not return self.
  • __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 with ExitStack.
  • 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 aclosing for async generators that need deterministic cleanup when iteration may end early.

Common Mistakes

  • Returning a resource created inside with after 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.