All articles
2 min read

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.