contextlib is the standard library module providing utilities for working with the with statement. Its centrepiece is the @contextmanager decorator, which builds a context manager from a generator function, plus helpers like suppress, closing, redirect_stdout, and ExitStack.
Context managers without the class
What you will learn: how @contextmanager turns a simple generator into a context manager — setup before yield, cleanup after.
How to read this tab: Always wrap the yield in try/finally so cleanup runs even when the with block raises.
Writing a context manager (something usable with with) normally means a class with __enter__ and __exit__. contextlib's @contextmanager lets you write one as a simple generator instead — setup code, then yield, then cleanup code. Much less boilerplate.
Before yield is setup, after yield is cleanup. @contextmanager turns a generator into a context manager. The yielded value becomes the as variable. This is far less code than a class with __enter__/__exit__ — the standard library uses it for most of its own context managers.
@contextmanager — the easy way
Decorate a generator with @contextmanager. Everything before yield is the setup (the __enter__ part); the yielded value is bound to the as variable; everything after yield is the cleanup (the __exit__ part).
from contextlib import contextmanager
import time
@contextmanager
def timer(label):
start = time.perf_counter() # setup (like __enter__)
try:
yield # the 'with' block runs here
finally:
elapsed = time.perf_counter() - start # cleanup (like __exit__)
print(f"{label}: {elapsed:.4f}s")
with timer("processing"):
total = sum(range(10_000_000))
# processing: 0.31s
# Yielding a value — it becomes the 'as' variable
@contextmanager
def open_upper(path):
f = open(path, encoding="utf-8")
try:
yield (line.upper() for line in f) # yielded -> 'as' target
finally:
f.close()
# with open_upper("data.txt") as upper_lines:
# for line in upper_lines:
# print(line)If the code inside the with block raises an exception, it propagates out through the yield. Wrapping yield in try/finally ensures your cleanup runs even when an exception occurs — exactly what a context manager is supposed to guarantee.
✅ Beginner tab complete
- I can write a context manager with @contextmanager and a generator
- I know code before yield is setup, after yield is cleanup
- I wrap yield in try/finally for guaranteed cleanup
- I know the generator must yield exactly once
suppress, closing, and redirecting output
What you will learn: suppress for cleanly ignoring expected exceptions, closing for objects with a close() method, and redirect_stdout for capturing print output.
How to read this tab: suppress is a cleaner try/except/pass — use it when you genuinely expect and want to ignore an exception.
suppress is a clean try/except/pass. with suppress(FileNotFoundError): os.remove(path) ignores the error if the file is absent. Clearer about intent than try/except/pass, but only use it when you genuinely expect and want to ignore the exception.
suppress and closing
Two small but constantly useful helpers: suppress silences specific exceptions cleanly, and closing ensures an object's close() method is called.
from contextlib import suppress, closing
import os
# suppress — cleaner than try/except/pass for expected exceptions
with suppress(FileNotFoundError):
os.remove("temp.txt") # no error if the file is absent
# Equivalent to:
# try:
# os.remove("temp.txt")
# except FileNotFoundError:
# pass
# Suppress multiple exception types
with suppress(FileNotFoundError, PermissionError):
os.remove("locked.txt")
# closing — call .close() on an object that lacks context-manager support
from urllib.request import urlopen
with closing(urlopen("https://example.com")) as page:
data = page.read()
# page.close() is called automatically on exitredirect_stdout captures print() into a buffer. Combine with io.StringIO to capture the output of code you do not control: with redirect_stdout(buffer): noisy_function(). The prints go into the buffer instead of the screen.
Redirecting stdout and stderr
from contextlib import redirect_stdout, redirect_stderr
import io
# Capture print() output into a string
buffer = io.StringIO()
with redirect_stdout(buffer):
print("This goes into the buffer")
print("So does this")
captured = buffer.getvalue()
print(f"Captured: {captured!r}")
# Captured: 'This goes into the buffer\nSo does this\n'
# Useful for capturing output of a function you don't control
def noisy_function():
print("debug spam")
return 42
buffer = io.StringIO()
with redirect_stdout(buffer):
result = noisy_function() # its prints are captured, not shown
print(f"Result was {result}, suppressed its output")
# Redirect to a file
with open("log.txt", "w") as f:
with redirect_stdout(f):
print("This line goes to log.txt")suppress is clearer about intent and more concise. Use suppress when you genuinely expect and want to ignore a specific exception — not as a way to hide bugs.✅ Intermediate tab complete
- I can use suppress(FileNotFoundError) instead of try/except/pass
- I can use closing() to ensure close() is called
- I can capture print output with redirect_stdout and io.StringIO
- I know suppress can take multiple exception types
ExitStack and dynamic context management
What you will learn: ExitStack for a runtime-determined number of context managers, nullcontext for conditional use, and the pop_all() ownership-transfer pattern.
How to read this tab: Read when you need to manage a variable number of resources, or build an object that acquires several resources safely.
ExitStack and dynamic context management
ExitStack manages an arbitrary, runtime-determined number of context managers — solving the problem that a with statement needs to know its context managers at write time. It guarantees that every entered context is exited in reverse order, even if an exception occurs partway through setup.
from contextlib import ExitStack, contextmanager, nullcontext
# Open a runtime-determined number of files, all cleaned up safely
def merge_files(filenames, output):
with ExitStack() as stack:
files = [stack.enter_context(open(f)) for f in filenames]
with open(output, "w") as out:
for f in files:
out.write(f.read())
# every file is closed here, in reverse order, even on error
# nullcontext — a do-nothing context manager for conditional use
def process(data, lock=None):
# Use the lock if provided, otherwise a no-op context
with (lock if lock is not None else nullcontext()):
return transform(data)
# ExitStack as a dynamic callback registry
with ExitStack() as stack:
stack.callback(print, "cleanup 3") # runs last
stack.callback(print, "cleanup 2")
stack.callback(print, "cleanup 1") # runs first (LIFO)
print("doing work")
# doing work / cleanup 1 / cleanup 2 / cleanup 3
# Defer cleanup conditionally — pop_all transfers ownership
def open_database(url):
with ExitStack() as stack:
conn = stack.enter_context(connect(url))
configure(conn) # if this raises, conn closes
# success — transfer cleanup responsibility to the caller
stack.pop_all() # conn will NOT be closed here
return conncontextlib also provides class-based building blocks: ContextDecorator lets a context manager double as a decorator, and AbstractContextManager/AbstractAsyncContextManager are the ABCs behind the protocol. For async code, @asynccontextmanager and AsyncExitStack mirror their synchronous counterparts for use with async with. The ExitStack.pop_all() pattern shown above is the idiomatic way to build an object that acquires several resources and hands cleanup responsibility to its caller — acquiring everything safely, and only disarming the automatic cleanup once construction fully succeeds.
✅ Expert tab complete
- I can use ExitStack to manage a dynamic number of context managers
- I can use nullcontext for conditional context management
- I understand the pop_all() pattern for transferring cleanup ownership
- I know @asynccontextmanager and AsyncExitStack exist for async