Context managers put resource acquisition and cleanup inside an explicit boundary. contextlib makes that guarantee concise when a try/finally block would be correct but repetitive.
Build a generator-based context
from contextlib import contextmanager
from pathlib import Path
from typing import Iterator, TextIO
@contextmanager
def open_report(path: Path) -> Iterator[TextIO]:
file = path.open("w", encoding="utf-8")
try:
yield file
finally:
file.close()
with open_report(Path("report.txt")) as report:
report.write("processing complete\n")
The value yielded becomes the target after as. A generator decorated with @contextmanager must yield exactly once. If setup fails before yield, the body never starts; if the body fails, the exception is raised again at the yield expression so the generator can log, translate, or propagate it.
@contextmanager
def transaction(connection):
cursor = connection.cursor()
try:
yield cursor
except Exception:
connection.rollback()
raise
else:
connection.commit()
finally:
cursor.close()
The else matters: commit only after a successful body, rollback on failure, and always close the cursor. Catching an exception without re-raising tells the with statement it was handled. That can be intentional for a narrowly documented case, but is dangerous as a default.
Use built-in adapters precisely
closing(resource) calls resource.close() on exit. Use it only for an object that needs closing but does not already implement the protocol. Many standard-library objects, including files and HTTP responses from suitable clients, already support with.
nullcontext(value) returns a context that does nothing and yields value. It is useful when an optional resource can be supplied by the caller:
from contextlib import nullcontext
def read_lines(source=None):
context = open("input.txt", encoding="utf-8") if source is None else nullcontext(source)
with context as stream:
return stream.readlines()
suppress(FileNotFoundError) can express that one specific absence is acceptable, such as deleting an optional cache file. Keep the protected block small. Broad suppression makes unrelated defects look like success.
redirect_stdout() and redirect_stderr() temporarily replace process-global streams. They help capture output from code that cannot receive a stream, especially in tests, but are not suitable for most concurrent or library code. Prefer passing an output stream or using structured logging when you control the function.
Compose dynamic resources with ExitStack
Several fixed contexts can share one with line. ExitStack becomes valuable when the count is determined at runtime or cleanup callbacks must join context managers.
from contextlib import ExitStack
from pathlib import Path
def concatenate(paths: list[Path], destination: Path) -> None:
with ExitStack() as stack:
inputs = [
stack.enter_context(path.open(encoding="utf-8"))
for path in paths
]
output = stack.enter_context(destination.open("w", encoding="utf-8"))
for stream in inputs:
output.write(stream.read())
If opening the third file fails, the first two are already registered and close automatically. Cleanup runs in last-in, first-out order, mirroring nested with statements. stack.callback(function, *args) registers an ordinary cleanup function, while pop_all() transfers the callbacks to a new stack when ownership must move after validation.
Avoid using ExitStack to hide a stable set of two resources; nested or comma-separated with is easier to read there. The stack earns its complexity when acquisition is conditional, iterative, or partly callback-based.
Async contexts and reusable designs
asynccontextmanager follows the same one-yield structure for async with. AsyncExitStack can combine asynchronous context managers and coroutine cleanup callbacks. Use these tools for resources whose acquisition or release is awaitable, not merely because the calling function is async.
A generator-based context manager instance is one-shot. The decorated function can create a fresh instance for every use. If a context must be reentrant or carry a substantial public API, a class implementing __enter__ and __exit__ may make state transitions clearer. AbstractContextManager and AbstractAsyncContextManager can provide useful base behavior.
Test both paths: normal completion and an exception raised inside the body. Also test partial acquisition when using a stack. Cleanup code should preserve the original error whenever possible; an exception raised during closing can otherwise obscure the failure that triggered cleanup.
Code before yield acquires the resource, while finally guarantees cleanup even if the with body raises. Do not swallow errors without an explicit policy. Logging and re-raising is usually safer than reporting false success.
closing() adapts objects that expose close() but lack the context protocol. suppress() is appropriate only for narrow, expected exceptions. Avoid suppress(Exception), which can hide programming failures.
When resources come from a collection, ExitStack can enter contexts in a loop and unwind them in reverse order. This is safer than collecting open files manually and attempting cleanup later.
Read the guide to Python exception handling for the surrounding error policy. The official contextlib documentation, accessed July 22, 2026, covers synchronous and asynchronous contexts, ExitStack, and redirection utilities.
Review cleanup as a contract
Document what the manager acquires, what it yields, which exceptions it translates, and whether it suppresses anything. Keep acquisition before yield small and register cleanup immediately after obtaining each resource so later setup failures cannot leak it. If cleanup can fail, decide whether that error or the original body exception should remain visible.
Use a fake resource that records calls. Assert the normal open, use, close order; then raise inside the body and confirm close still happens. For ExitStack, fail midway through acquisition and check that earlier items are released in reverse order. Test transaction commit and rollback separately.
Do not wrap ordinary computation in a context merely for visual symmetry. Context management is strongest when it represents ownership or a temporary state change with a clear restoration rule. A narrow contract makes failure predictable and keeps cleanup details away from callers.