The contextlib module provides utilities for working with context managers. Beyond the basic with statement, these tools enable elegant resource management patterns.

@contextmanager Decorator

Turn any generator into a context manager:

from contextlib import contextmanager
 
@contextmanager
def timer(label):
    import time
    start = time.perf_counter()
    try:
        yield  # Code in 'with' block runs here
    finally:
        elapsed = time.perf_counter() - start
        print(f"{label}: {elapsed:.3f}s")
 
# Usage
with timer("database query"):
    result = db.query("SELECT * FROM users")

The pattern:

  1. Setup code before yield
  2. yield (optionally with a value)
  3. Cleanup code after yield (in finally)

Yielding Values

from contextlib import contextmanager
import tempfile
import os
 
@contextmanager
def temp_directory():
    path = tempfile.mkdtemp()
    try:
        yield path
    finally:
        import shutil
        shutil.rmtree(path)
 
with temp_directory() as tmpdir:
    print(f"Working in {tmpdir}")
    # Create files, do work
# Directory automatically cleaned up

suppress: Ignoring Exceptions

from contextlib import suppress
 
# Instead of:
try:
    os.remove('file.txt')
except FileNotFoundError:
    pass
 
# Use:
with suppress(FileNotFoundError):
    os.remove('file.txt')
 
# Multiple exceptions
with suppress(FileNotFoundError, PermissionError):
    os.remove('file.txt')

Clean and explicit when you intentionally want to ignore specific exceptions.

redirect_stdout / redirect_stderr

Capture or redirect output:

from contextlib import redirect_stdout, redirect_stderr
import io
 
# Capture stdout
buffer = io.StringIO()
with redirect_stdout(buffer):
    print("This goes to buffer")
    
output = buffer.getvalue()
print(f"Captured: {output}")
 
# Redirect to file
with open('log.txt', 'w') as f:
    with redirect_stdout(f):
        print("This goes to file")
 
# Suppress output entirely
with redirect_stdout(io.StringIO()):
    noisy_function()

ExitStack: Dynamic Context Management

Manage a dynamic number of context managers:

from contextlib import ExitStack
 
def process_files(filenames):
    with ExitStack() as stack:
        files = [
            stack.enter_context(open(fname))
            for fname in filenames
        ]
        # All files open, process them
        for f in files:
            process(f)
    # All files automatically closed
 
# Works with any number of files
process_files(['a.txt', 'b.txt', 'c.txt'])

ExitStack Callbacks

from contextlib import ExitStack
 
with ExitStack() as stack:
    # Register cleanup callback
    stack.callback(print, "Cleanup 1")
    stack.callback(print, "Cleanup 2")
    
    # Do work
    print("Working...")
    
# Output:
# Working...
# Cleanup 2
# Cleanup 1  (LIFO order)

ExitStack Exception Handling

from contextlib import ExitStack
 
with ExitStack() as stack:
    stack.enter_context(resource1)
    stack.enter_context(resource2)  # If this fails...
    # resource1 is still properly cleaned up

closing: Add exit to Objects

For objects with close() but no context manager support:

from contextlib import closing
import urllib.request
 
# urllib.request.urlopen() returns something with close() but no __exit__
with closing(urllib.request.urlopen('http://example.com')) as page:
    html = page.read()
# Automatically closed

nullcontext: No-Op Context Manager

Useful for conditional context managers:

from contextlib import nullcontext
 
def process(data, lock=None):
    with lock if lock else nullcontext():
        # Process data
        pass
 
# With locking
process(data, threading.Lock())
 
# Without locking (nullcontext does nothing)
process(data)

Python 3.10+ allows optional value:

from contextlib import nullcontext
 
with nullcontext("default") as value:
    print(value)  # "default"

chdir: Temporary Directory Change

Python 3.11+:

from contextlib import chdir
import os
 
print(os.getcwd())  # /home/user
 
with chdir('/tmp'):
    print(os.getcwd())  # /tmp
    # Do work in /tmp
 
print(os.getcwd())  # /home/user (restored)

AsyncExitStack

For async context managers:

from contextlib import asynccontextmanager, AsyncExitStack
 
@asynccontextmanager
async def async_resource():
    print("Acquiring")
    yield "resource"
    print("Releasing")
 
async def main():
    async with AsyncExitStack() as stack:
        r1 = await stack.enter_async_context(async_resource())
        r2 = await stack.enter_async_context(async_resource())
        # Use r1, r2

Practical Example: Database Transaction

from contextlib import contextmanager
 
@contextmanager
def transaction(connection):
    cursor = connection.cursor()
    try:
        yield cursor
        connection.commit()
    except Exception:
        connection.rollback()
        raise
    finally:
        cursor.close()
 
# Usage
with transaction(db_connection) as cursor:
    cursor.execute("INSERT INTO users VALUES (?)", (name,))
    cursor.execute("UPDATE counts SET n = n + 1")
# Auto-commits or rolls back

Quick Reference

UtilityPurpose
@contextmanagerCreate context manager from generator
suppress(*exc)Ignore specific exceptions
redirect_stdout(f)Redirect print to file/buffer
redirect_stderr(f)Redirect stderr
ExitStackManage dynamic context managers
closing(obj)Wrap object with close() method
nullcontext(val)No-op context manager
chdir(path)Temporary directory change (3.11+)
AsyncExitStackAsync version of ExitStack
from contextlib import (
    contextmanager,
    suppress,
    redirect_stdout,
    ExitStack,
    closing,
    nullcontext,
)

contextlib turns resource management patterns into clean, reusable code. If you're writing try/finally blocks, there's probably a contextlib solution.

React to this post: