Understanding Python's contextlib for Context Management
Python's contextlib module provides utilities for creating and working with context managers: @contextmanager, closing(), suppress(), redirect_stdout, and ExitStack.
A context manager defines what happens when entering and leaving a with block — typically acquiring a resource on entry and releasing it on exit. Python's contextlib module provides tools to create custom context managers without writing a full class, and several useful utilities for common patterns.
Creating Context Managers with @contextmanager
The @contextmanager decorator turns a generator function into a context manager. Everything before yield runs on entry; everything after runs on exit:
from contextlib import contextmanager
@contextmanager
def managed_connection(host):
print(f"Connecting to {host}")
conn = {"host": host, "open": True} # simulate connection
try:
yield conn
finally:
conn["open"] = False
print(f"Disconnected from {host}")
with managed_connection("db.example.com") as conn:
print(f"Using connection: {conn}")
# Output:
# Connecting to db.example.com
# Using connection: {'host': 'db.example.com', 'open': True}
# Disconnected from db.example.com
The finally block runs even if an exception is raised inside the with block, ensuring cleanup always happens.
closing() — Ensure .close() Is Called
For objects that have a .close() method but don't implement the context manager protocol:
from contextlib import closing
import urllib.request
with closing(urllib.request.urlopen("https://example.com")) as page:
content = page.read()
# page.close() is called automatically here
Any object with a .close() method works, including database cursors, sockets, and custom classes.
suppress() — Silently Ignore Specific Exceptions
Instead of a try/except block that swallows an exception, use suppress:
from contextlib import suppress
import os
# Without suppress:
try:
os.remove("temp.txt")
except FileNotFoundError:
pass
# With suppress (cleaner):
with suppress(FileNotFoundError):
os.remove("temp.txt")
Only suppress exceptions you've explicitly decided to ignore. Don't use it as a blanket error handler.
redirect_stdout and redirect_stderr
Temporarily redirect standard output or error to another stream — useful for capturing output in tests or logging:
from contextlib import redirect_stdout
import io
buffer = io.StringIO()
with redirect_stdout(buffer):
print("This goes to the buffer, not the console")
print("Same here")
output = buffer.getvalue()
print(f"Captured: {output!r}")
# Captured: 'This goes to the buffer, not the console\nSame here\n'
ExitStack — Dynamic Stack of Context Managers
When you need to enter a variable number of context managers, ExitStack lets you build the stack dynamically:
from contextlib import ExitStack
files = ["a.txt", "b.txt", "c.txt"]
with ExitStack() as stack:
handles = [stack.enter_context(open(f, "w")) for f in files]
for i, fh in enumerate(handles):
fh.write(f"content {i}\n")
# All three files are closed here
ExitStack is also useful for conditionally entering a context manager:
with ExitStack() as stack:
if needs_transaction:
stack.enter_context(db.transaction())
# rest of the code
Choosing the Right Tool
| Need | Use |
|---|---|
| Custom resource lifecycle | @contextmanager |
| Object with .close() but no with support | closing() |
| Ignore a specific exception silently | suppress() |
| Capture/redirect print output | redirect_stdout |
| Variable number of context managers | ExitStack |
Conclusion
contextlib eliminates the boilerplate of writing __enter__/__exit__ classes for every resource. Use @contextmanager for custom lifecycle management, suppress() to cleanly handle expected exceptions, and ExitStack when the number of context managers isn't known until runtime.