🐍 Python Course · Stage 3 · Lesson 46 of 89 · contextlib — with Utilities
Course Overview →

🔵 Read Beginner and Intermediate tabs. The Expert tab has the internals — worth reading once you finish Stage 3.

← random — Random Numbers secrets — Secure Random →
thecodex.expert · The Codex Family of Knowledge
Python Standard Library

contextlib — Utilities for with-Statements

contextlib provides tools for creating and combining context managers — most importantly @contextmanager, which turns a simple generator into a full context manager.

import contextlib docs.python.org Last verified:
Canonical Definition

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.

🟩 Beginner

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.

⏱ 25 min📄 1 section🔶 Prerequisite: Context Managers
The idea

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).

Pythoncontextmanager.py
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)
Always wrap yield in try/finally

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

Continue to functools →

🔵 Intermediate

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.

⏱ 25 min📄 2 sections🔶 Prerequisite: Beginner tab
💡

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.

Pythonsuppress_closing.py
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 exit
💡

redirect_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

Pythonredirect.py
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")
Commonly confused
suppress vs try/except/pass. They do the same thing, but 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.
@contextmanager generators must yield exactly once. If the generator yields zero times or more than once, you get a RuntimeError. The single yield marks the boundary between setup and cleanup.

✅ 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

Continue to functools →

🔴 Expert

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.

⏱ 20 min📄 1 section🔶 Prerequisite: After Stage 2

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.

Pythonexitstack.py
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 conn

contextlib 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

Continue to functools →

Sources

1
Python Standard Library — contextlib. docs.python.org/3/library/contextlib.html.
2
Python Standard Library — contextlib.contextmanager. docs.python.org/3/library/contextlib.html#contextlib.contextmanager.
3
Python Standard Library — contextlib.ExitStack. docs.python.org/3/library/contextlib.html#contextlib.ExitStack.
4
PEP 343 — The "with" Statement. peps.python.org/pep-0343/.
Source confidence: High Last verified: Primary source: docs.python.org contextlib