Mastering Context Managers in Python: From the `with` Statement to `contextlib`

Learn how Python's `with` statement really works and how to build your own context managers three ways — the `__enter__`/`__exit__` protocol, the `@contextmanager` decorator, and standard-library tools like `suppress`, `redirect_stdout`, and `ExitStack`.

Mastering Context Managers in Python: From the `with` Statement to `contextlib`

Almost every Python developer has typed with open(...) as f: without giving the with keyword a second thought. But context managers are one of the most underrated tools in the language. They are how Python guarantees that setup and cleanup happen as a pair — files get closed, locks get released, transactions get committed or rolled back — even when an exception blows a hole through the middle of your code. Once you understand how they work, you will start reaching for them to model all kinds of "do this, then always undo it" patterns in your own programs.

This guide walks through the protocol behind with, shows you three different ways to build your own context managers, and tours the most useful tools in the standard library's contextlib module.

The problem context managers solve

Consider opening a file the manual way. You have to remember to close it, and you have to close it even if an error occurs while you are writing:

f = open("data.txt", "w")
try:
    f.write("hello")
finally:
    f.close()  # runs whether or not write() raised

That try/finally block is correct but noisy, and it is easy to forget. The with statement collapses the whole pattern into one line and makes the cleanup automatic:

with open("data.txt", "w") as f:
    f.write("hello")
# f is guaranteed closed here, exception or not

The magic is not specific to files. Any object that implements the context manager protocol can be used with with.

The protocol: __enter__ and __exit__

A context manager is simply an object with two methods. __enter__ runs at the top of the with block and its return value is bound to the name after as. __exit__ runs when the block ends — normally or via an exception. Here is a reusable timer that measures how long its block takes:

import time

class Timer:
    def __enter__(self):
        self.start = time.perf_counter()
        return self  # bound to the 'as' target

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.elapsed = time.perf_counter() - self.start
        print(f"Elapsed: {self.elapsed:.6f}s")
        return False  # don't suppress exceptions

with Timer() as t:
    sum(range(1_000_000))
# prints something like: Elapsed: 0.012345s

Notice the three arguments to __exit__: the exception type, the exception value, and the traceback. If the block exits normally, all three are None. If an exception occurred, they describe it — which gives __exit__ the chance to inspect and even swallow it.

Suppressing exceptions from __exit__

If __exit__ returns a truthy value, Python treats the exception as handled and execution continues after the with block. Return False (or nothing) and the exception propagates as usual. This is a sharp tool — use it deliberately:

class IgnoreDivisionErrors:
    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        if exc_type is ZeroDivisionError:
            print("Ignored:", exc_val)
            return True   # swallow it
        return False      # let everything else through

with IgnoreDivisionErrors():
    1 / 0
print("Execution continues normally")

The easier way: contextlib.contextmanager

Writing a whole class for a simple setup/teardown is overkill. The contextlib.contextmanager decorator lets you build a context manager from a single generator function. Everything before the yield is the "enter" phase; everything after is the "exit" phase. The value you yield becomes the as target.

from contextlib import contextmanager

@contextmanager
def managed_resource(name):
    print(f"Acquiring {name}")
    resource = {"name": name}
    try:
        yield resource          # hand control to the with-block
    finally:
        print(f"Releasing {name}")

with managed_resource("database") as r:
    print("Using", r["name"])

# Acquiring database
# Using database
# Releasing database

The try/finally is what makes cleanup bulletproof: if the body of the with raises, the exception is re-raised at the point of the yield, so your finally still runs. This pattern shines for things like database transactions, where you want to commit on success and roll back on failure:

@contextmanager
def transaction(conn):
    print("BEGIN")
    try:
        yield conn
    except Exception as exc:
        print("ROLLBACK:", exc)
        raise          # re-raise so the caller still sees the error
    else:
        print("COMMIT")

# On success -> BEGIN, COMMIT
# On error   -> BEGIN, ROLLBACK, and the exception propagates

The else branch of a try only runs when no exception occurred, making it the perfect home for the commit. Re-raising in the except branch ensures you handle cleanup without silently hiding bugs from the caller.

Batteries included: useful tools in contextlib

The contextlib module ships several ready-made helpers that remove boilerplate from everyday code.

suppress: ignore a specific exception

Instead of an empty except block, suppress states your intent clearly. This is much cleaner than try/except/pass:

from contextlib import suppress
import os

with suppress(FileNotFoundError):
    os.remove("maybe_missing.tmp")
# No error if the file was never there

redirect_stdout: capture print output

Temporarily redirect everything printed to standard output into a buffer — handy for testing functions that print, or for capturing output from code you do not control:

from contextlib import redirect_stdout
import io

buffer = io.StringIO()
with redirect_stdout(buffer):
    print("This goes into the buffer")

print("Captured:", repr(buffer.getvalue()))
# Captured: 'This goes into the buffer\n'

ExitStack: manage a dynamic number of resources

Sometimes you do not know at write-time how many resources you will open — say, a list of files whose length depends on user input. Nesting with statements does not work when the count is dynamic. ExitStack solves this by letting you register context managers on the fly; it unwinds them in reverse order when the block ends:

from contextlib import ExitStack

filenames = ["a.txt", "b.txt", "c.txt"]

with ExitStack() as stack:
    files = [stack.enter_context(open(name, "w")) for name in filenames]
    for f in files:
        f.write("data")
# All three files are closed here, in reverse order,
# even if one of the writes had failed.

Common pitfalls

A few mistakes catch people repeatedly. First, a @contextmanager generator must yield exactly once. If it yields zero times or more than once, you will get a RuntimeError. Second, always wrap the yield in try/finally if cleanup must happen — without it, an exception in the body will skip your teardown code entirely. Third, returning a truthy value from __exit__ silently swallows exceptions; do this only when you genuinely mean to, because accidentally hiding errors leads to baffling bugs. Finally, remember that the object returned by __enter__ is not always the context manager itself — for files it is the file object, but for a class-based manager it is whatever you choose to return.

Wrap-up and next steps

Context managers turn the fragile "remember to clean up" pattern into a guarantee enforced by the language. You now have three tools for building them: the explicit __enter__/__exit__ protocol for full control, the @contextmanager decorator for quick generator-based managers, and the prebuilt helpers in contextlib like suppress, redirect_stdout, and ExitStack.

To go further, explore contextlib.closing for objects that have a close() method but no native with support, and look into writing context managers that also work with async with using __aenter__ and __aexit__ (or contextlib.asynccontextmanager). The next time you find yourself writing a try/finally, ask whether a context manager would express the intent more clearly — more often than not, it will.