contextlib
Writing a full __enter__/__exit__ class for every context manager is often overkill — the standard library's contextlib module provides ready-made helpers and a decorator that builds one from a single generator function.
The contextlib standard library module provides tools that make context managers easier to write and combine: @contextmanager turns an ordinary generator function into a context manager without writing a class, closing() and suppress() wrap common patterns in one call, redirect_stdout/redirect_stderr retarget output temporarily, and ExitStack/AsyncExitStack manage a dynamic, variable number of context managers at once.
@contextlib.contextmanager
@contextmanager decorates a generator function that yields exactly once. Code before the yield runs as __enter__; the yielded value is what as name binds to; code after the yield runs as __exit__ when the with block ends. This avoids writing a separate class with two methods for what is often a simple linear setup/teardown sequence.
Everything before yield is setup, everything after is teardown; wrapping the yield in try/finally ensures the teardown (timing report) runs even if the with block's body raises.
Always wrap the yield in try/finally inside a @contextmanager function whenever cleanup must happen regardless of exceptions — the decorator does not add this for you automatically.
from contextlib import contextmanager
import time
@contextmanager
def timer(label: str):
start = time.perf_counter()
try:
yield
finally:
elapsed = time.perf_counter() - start
print(f"{label}: {elapsed:.4f}s")
with timer("sum"):
total = sum(range(10_000_000))Handling Exceptions Inside a @contextmanager Function
If the with block's body raises, the exception is thrown into the generator at the yield point (as if yield itself raised it). Catching it there lets the generator function inspect or suppress it, mirroring what a class-based __exit__ can do by returning True.
Catching the exception at the yield point and NOT re-raising it is equivalent to a class-based __exit__ returning True — the exception is suppressed and execution continues after the with block.
@contextmanager
def suppress_zero_division():
try:
yield
except ZeroDivisionError:
print("suppressed a division by zero")
# not re-raising here suppresses it, same as __exit__ returning True
with suppress_zero_division():
1 / 0
print("still running")contextlib.closing
closing(thing) wraps any object that has a .close() method but isn't itself a context manager, guaranteeing .close() is called when the with block exits. Useful for older APIs or objects that predate the context manager protocol.
urlopen()'s return value supports .close() but relying on that directly (without with) means you'd have to remember to call it manually; closing() gives it with-statement guarantees.
from contextlib import closing
from urllib.request import urlopen
with closing(urlopen("https://example.com")) as response:
html = response.read()
# response.close() called automaticallycontextlib.suppress
suppress(*exception_types) is a ready-made context manager that ignores the listed exception types if they occur inside the block, and lets any other exception propagate normally. It replaces the common try: ... except SomeError: pass pattern with something more explicit about intent.
suppress(FileNotFoundError) reads clearly as 'it's fine if this file doesn't exist' — more explicit and self-documenting than a bare try/except/pass block.
from contextlib import suppress
from pathlib import Path
with suppress(FileNotFoundError):
Path("maybe_missing.txt").unlink()
# no error if the file didn't exist; other errors (e.g. PermissionError) still propagateredirect_stdout and redirect_stderr
redirect_stdout(target) and redirect_stderr(target) temporarily reroute sys.stdout/sys.stderr to a file-like object (commonly io.StringIO() for capturing output in memory, e.g. in tests) for the duration of the with block, restoring the original afterward.
Inside the with block, print() writes into buffer instead of the terminal; outside it, stdout is restored to normal and captured_text holds whatever was written during the block.
import io
from contextlib import redirect_stdout
buffer = io.StringIO()
with redirect_stdout(buffer):
print("captured, not printed to the terminal")
captured_text = buffer.getvalue()
print(captured_text) # this one IS printed normally, after the blockExitStack for a Dynamic Number of Context Managers
ExitStack manages a variable, runtime-determined number of context managers, entering each via enter_context() and guaranteeing all of them exit (in reverse order) when the stack's own with block ends — something a fixed with a, b, c: statement can't express when the count isn't known ahead of time.
The number of files isn't known until process_all() runs; ExitStack lets each be entered dynamically while still guaranteeing all are properly closed, even if reading one of them raises partway through.
ExitStack also supports callback(fn, *args, **kwargs) to register a plain cleanup function (not a full context manager) to run on exit, and pop_all() to transfer ownership of already-entered resources to a new stack.
from contextlib import ExitStack
def process_all(paths: list[str]) -> list[str]:
with ExitStack() as stack:
files = [stack.enter_context(open(p)) for p in paths]
return [f.read() for f in files]
# every file in `files` is closed here, however many there wereAsyncExitStack
AsyncExitStack is the async-aware counterpart, used inside async with blocks with enter_async_context() (for async context managers) alongside enter_context() (for regular ones) — useful when managing a dynamic mix of async resources such as multiple database connections.
Mirrors ExitStack but for async context managers, guaranteeing every entered connection is properly closed via its __aexit__ when the async with block ends — related concept: async/await, covered separately.
from contextlib import AsyncExitStack
async def process_all_async(paths: list[str]):
async with AsyncExitStack() as stack:
conns = [await stack.enter_async_context(open_conn(p)) for p in paths]
return [await c.read() for c in conns]Choosing Between a Class and @contextmanager
@contextmanager is usually less code and reads more linearly for simple setup/teardown pairs. A full class is worth writing when the context manager needs to hold significant state accessed by other methods, be reused/instantiated with different configurations repeatedly in a structured way, or when the class already exists for other reasons and just needs the protocol added.
A generator-based context manager here reads top-to-bottom as a single procedure, avoiding the ceremony of a class with __init__, __enter__, and __exit__ for what is fundamentally linear logic.
# @contextmanager: concise for simple, linear setup/teardown
@contextmanager
def temp_file(content: str):
import tempfile, os
fd, path = tempfile.mkstemp()
try:
with os.fdopen(fd, "w") as f:
f.write(content)
yield path
finally:
os.remove(path)Key Takeaways
@contextmanager builds a context manager from a generator function: code before yield is __enter__, code after (in finally) is __exit__.
closing(obj) gives with-statement guarantees to any object with a .close() method but no native context manager support.
suppress(*types) ignores specific exception types inside a block, replacing a manual try/except/pass with clearer intent.
redirect_stdout/redirect_stderr temporarily reroute output to a file-like object, commonly io.StringIO() for testing.
ExitStack (and AsyncExitStack for async code) manages a dynamic, runtime-determined number of context managers, guaranteeing all exit properly.
@contextmanager def resource(): r = acquire() yield r release(r) # skipped if the with block raises
@contextmanager def resource(): r = acquire() try: yield r finally: release(r)
Without wrapping yield in try/finally, an exception in the with block's body skips the cleanup code after yield entirely; @contextmanager does not add this protection automatically.
try: Path(p).unlink() except FileNotFoundError: pass with suppress(FileNotFoundError): Path(p).unlink()
Functionally equivalent, but suppress() states the intent to ignore that specific exception more explicitly and concisely than a manual try/except/pass block.
files = [] for p in paths: files.append(open(p)) # never explicitly closed with ExitStack() as stack: files = [stack.enter_context(open(p)) for p in paths] # use files here
Opening a runtime-determined number of files without a with-statement equivalent leaves cleanup to chance; ExitStack guarantees every one of them is closed when its block exits.
How does @contextmanager turn a generator function into a context manager?
Code before the single yield runs as __enter__ (and the yielded value becomes what as name binds to); code after the yield runs as __exit__ when the with block ends, whether normally or via an exception thrown into the generator at the yield point.
Why must a @contextmanager function wrap its yield in try/finally to guarantee cleanup?
Because @contextmanager doesn't add exception safety automatically — if the with block's body raises, the exception is thrown in at the yield statement, and without try/finally around it, any cleanup code written after yield would be skipped entirely.
What does contextlib.suppress(FileNotFoundError) do differently from a bare except FileNotFoundError: pass?
Functionally very similar, but suppress() is a ready-made context manager used with with, reading more explicitly as 'this specific exception is expected and fine to ignore', and requires less boilerplate than writing a try/except block.
When would you use ExitStack instead of a plain with a, b, c: statement?
When the number of context managers to manage isn't known until runtime (e.g. opening a list of files whose length depends on input) — ExitStack lets you enter each dynamically via enter_context() while still guaranteeing all are exited properly.
What is AsyncExitStack used for, and how does it differ from ExitStack?
It's the async-aware equivalent, used inside async with blocks; it adds enter_async_context() for entering async context managers (those with __aenter__/__aexit__) alongside the regular enter_context() for sync ones.
What does redirect_stdout(io.StringIO()) accomplish?
It temporarily reroutes sys.stdout to the given StringIO object for the duration of the with block (capturing anything printed into it in memory instead of the terminal), and restores the original stdout when the block exits.
Write a @contextmanager function suppressed_output() that redirects both stdout and stderr to an io.StringIO and returns it, restoring both afterward even if the block raises.
import io import sys from contextlib import contextmanager @contextmanager def suppressed_output(): buffer = io.StringIO() old_stdout, old_stderr = sys.stdout, sys.stderr sys.stdout = sys.stderr = buffer try: yield buffer finally: sys.stdout, sys.stderr = old_stdout, old_stderr
Saves the original stdout/stderr, redirects both to the shared buffer, yields the buffer for inspection, and restores the originals in finally so restoration happens even if the block raises.
What is printed, in order?
'enter', then 'value: 42', then 'exit' — code before yield runs as setup, the yielded 42 is bound to value, and code after yield runs as teardown once the block finishes.
Write a function that opens a dynamic list of files using ExitStack and returns their combined line count, guaranteeing all files are closed even if one fails to read.
from contextlib import ExitStack def total_lines(paths: list[str]) -> int: with ExitStack() as stack: files = [stack.enter_context(open(p)) for p in paths] return sum(len(f.readlines()) for f in files)
ExitStack enters every file in the dynamically-sized list and guarantees all are closed when the with block exits, whether the readlines() loop completes or raises partway through.
from contextlib import contextmanager
@contextmanager
def cm():
print("enter")
yield 42
print("exit")
with cm() as value:
print("value:", value)