Home Tutorials Standard Library

Standard Library

Python's contextlib: Building Context Managers Without a Class

Pyford Notes July 6, 2026 7 min read
Key points
  • @contextmanager turns a generator function into a context manager, splitting it at yield into setup and teardown.
  • contextlib.suppress() replaces a try/except block whose only job is to ignore one specific exception type.
  • ExitStack manages a number of context managers decided at runtime, closing them all in reverse order even if one fails partway through.
  • redirect_stdout and redirect_stderr temporarily reroute output, which is genuinely useful for capturing output in tests.

A quick recap of the context manager protocol

A with block calls __enter__ on entry and __exit__ on exit, guaranteeing the exit code runs even if the block raises. The context managers article covers writing this as a class with both dunder methods. contextlib is the standard library's toolkit for the common cases where writing a full class is more ceremony than the problem needs.

@contextmanager and yield

The @contextmanager decorator turns an ordinary generator function into a context manager. Code before the yield is the setup, the yielded value is what as binds to, and code after the yield is the teardown:

from contextlib import contextmanager
import time

@contextmanager
def timer(label):
    start = time.perf_counter()
    try:
        yield
    finally:
        elapsed = time.perf_counter() - start
        print(f"{label}: {elapsed:.3f}s")

with timer("parsing"):
    data = [x * x for x in range(1_000_000)]

The try/finally around the yield matters: if the code inside the with block raises, the exception propagates up through the yield statement itself, and without the finally the teardown code would never run. This one function replaces what would otherwise be a class with __init__, __enter__, and __exit__ for a case this simple.

suppress for expected exceptions

A common pattern is a try/except block whose except clause does nothing but pass, deliberately ignoring one expected exception type:

try:
    import ujson as json_lib
except ImportError:
    pass

contextlib.suppress() expresses the same intent more directly:

from contextlib import suppress

with suppress(FileNotFoundError):
    os.remove("cache.tmp")

This reads as "removing this file might fail because it might not exist, and that's fine" in a single line, rather than a four-line try/except that a reader has to parse to confirm nothing else is being silently swallowed. It also makes the intent explicit for exactly one exception type, which is safer than a bare except: that would hide unrelated bugs too — the same care around specificity that the exception handling guide covers for regular try/except blocks. suppress() accepts several exception types at once, exactly like a tuple passed to except: suppress(FileNotFoundError, PermissionError) ignores either.

One limitation worth knowing: suppress() swallows the exception for the rest of the with block, not just the single line that might raise it. If a block contains two calls that could each raise the suppressed exception, and the first one does, the second call never runs at all — the block exits early, silently, at the point of the exception. For anything more than a single risky statement, a full try/except with an explicit pass makes that early exit visible in a way a one-line with suppress(...): does not.

ExitStack for a variable number of resources

A fixed number of resources nests naturally: with open(a) as fa, open(b) as fb:. But when the number of resources to open is only known at runtime — a list of file paths passed in as an argument — nesting doesn't work. ExitStack handles this:

from contextlib import ExitStack

def read_all(paths):
    with ExitStack() as stack:
        files = [stack.enter_context(open(p)) for p in paths]
        return [f.read() for f in files]

Every context manager passed to stack.enter_context() gets entered immediately and is guaranteed to be exited, in reverse order, when the with block ends — including if one of the later open() calls fails partway through the list, which is the scenario a hand-rolled loop with manual cleanup gets wrong most often.

redirect_stdout and redirect_stderr

Some functions and third-party libraries print directly to standard output with no way to capture the result as a return value. redirect_stdout reroutes that output temporarily:

from contextlib import redirect_stdout
import io

buffer = io.StringIO()
with redirect_stdout(buffer):
    help(str.split)

output = buffer.getvalue()

This shows up most often in tests that assert on printed output, or in tooling that wraps a noisy third-party function and wants to log its output instead of letting it hit the console directly. redirect_stderr works identically for the error stream. The contextlib documentation lists several more utilities in the same family, including nullcontext for conditionally skipping a context manager without branching the calling code.