pyguides

contextlib Context Manager Utilities

Why contextlib

Every Python developer has written a try/finally block to clean up a file, a lock, or a database connection. The contextlib context manager utilities replace most of those blocks with a one-liner, and that one-liner can be a generator, a class, an async coroutine, or a dynamic stack. The module gives you decorators that turn generators into context managers, ready-made managers for common jobs like suppressing exceptions and redirecting output, and dynamic cleanup stacks for situations that don’t fit the one-resource-one-with pattern.

This guide assumes you already know the with statement and the __enter__/__exit__ protocol. If you don’t, read the context managers guide and the __enter__ and __exit__ reference first, then come back. For an exhaustive list of every utility with signatures, see the contextlib module reference. The official Python contextlib documentation is the source of truth for signatures and version history.

What does @contextmanager do?

@contextmanager turns a generator function into a context manager. You write the setup, yield a value, and write the teardown. The decorator handles __enter__ and __exit__ for you.

from contextlib import contextmanager
import tempfile, os, shutil

@contextmanager
def temp_dir():
    path = tempfile.mkdtemp()
    try:
        yield path
    finally:
        shutil.rmtree(path, ignore_errors=True)

with temp_dir() as d:
    path = os.path.join(d, "scratch.txt")
    with open(path, "w") as f:
        f.write("scratch")
print(os.path.exists(d))  # output: False

The function must yield exactly once. Zero yields raises RuntimeError("generator didn't yield"). Two yields raises RuntimeError("generator didn't stop") on the second next(). There is no way to “yield nothing” or “yield twice”. Pick a different pattern.

Catching an exception inside the generator suppresses it. A try/except around the yield that does not re-raise behaves like __exit__ returning a truthy value. Always re-raise (or use raise from after logging) if you want the exception to propagate to the caller.

Since Python 3.2, @contextmanager also works as a function decorator. When you write @my_cm(), a fresh generator is created per call, so the one-shot rule does not apply for that use.

What does @asynccontextmanager do?

@asynccontextmanager is the same idea for async with. The decorated function must be an async def that contains a single yield. Use it for async database connections, HTTP sessions, and timers around coroutines.

import time
from contextlib import asynccontextmanager

@asynccontextmanager
async def timeit(label="block"):
    start = time.monotonic()
    try:
        yield
    finally:
        print(f"{label}: {time.monotonic() - start:.3f}s")

# output (timing varies): block: 0.000s

The decorator form became usable in 3.10. Before 3.10 you could only use it with async with.

Suppress, nullcontext, closing, and aclosing

These four are the small, single-purpose managers you will reach for most often.

suppress(*exceptions) is a reentrant way to silence specific errors. Since 3.12 it also understands BaseExceptionGroup and splits off the listed types.

import os
from contextlib import suppress

with suppress(FileNotFoundError):
    os.remove("/tmp/leftover.sock")
print("done")  # output: done

nullcontext(enter_result=None) is a no-op manager. __enter__ returns whatever you pass it. It is the cleanest way to make an “optional context manager” parameter work uniformly:

from contextlib import nullcontext

def write_data(data, transaction=None):
    cm = transaction or nullcontext()
    with cm:
        print(f"writing {data}")

write_data("hello")
# output: writing hello

class Tx:
    def __enter__(self): print("tx begin"); return self
    def __exit__(self, *a): print("tx commit")

write_data("world", transaction=Tx())
# output:
# tx begin
# writing world
# tx commit

closing(obj) calls obj.close() on exit. It does not call __exit__. Use it on legacy objects that have a close() method but no __enter__. Do not wrap objects that already support with in it.

from contextlib import closing

class LegacyResource:
    def __init__(self):
        self.closed = False
    def close(self):
        self.closed = True

with closing(LegacyResource()) as r:
    print("using", r.closed)  # output: using False
print(r.closed)  # output: True

aclosing(agen) is the async sibling: it calls await agen.aclose() on exit. Use it when an async generator might break out early, otherwise the generator’s cleanup code never runs.

import asyncio
from contextlib import aclosing

async def stream_events():
    for i in range(10):
        yield i
        if i == 3:
            return

async def main():
    async with aclosing(stream_events()) as events:
        async for event in events:
            if event == 3:
                break  # aclose() still runs deterministically

asyncio.run(main())
print("done")  # output: done

redirect_stdout and redirect_stderr

These replace sys.stdout (or sys.stderr) for the duration of the block, and return the new target from __enter__. They are the standard way to capture text-mode output from a function that prints to the console.

import io, contextlib

buf = io.StringIO()
with contextlib.redirect_stdout(buf):
    print("captured")
print(buf.getvalue())  # output: captured

Both are reentrant but not thread-safe. They mutate a process-global, so concurrent threads will not see consistent output. They also have no effect on subprocess output; if you need that, capture at the file-descriptor level.

The chdir context manager (3.11+)

Added in 3.11, chdir(path) changes the working directory on enter and restores the previous one on exit. Useful for tests that need to operate in a specific directory.

import os
from contextlib import chdir

with chdir("/tmp"):
    print(os.getcwd())  # output: /tmp
print(os.getcwd())  # output: <previous cwd>

Like the redirect managers, chdir is not thread-safe and not safe inside generators that yield. The cwd is global state, so a suspended coroutine with a changed cwd is a recipe for confusion.

How does ExitStack handle dynamic cleanup?

ExitStack is for situations where the number of resources is not known until runtime. The most common case is opening N files where any one failing should close the rest.

from contextlib import ExitStack

def open_many(paths):
    with ExitStack() as stack:
        files = [stack.enter_context(open(p)) for p in paths]
        yield files

with open_many(["/etc/hostname", "/etc/hosts"]) as files:
    print(len(files), files[0].readline().strip())

# output (line content varies): 2 <first line of /etc/hostname>

enter_context(cm) calls cm.__enter__(), pushes cm.__exit__ onto the stack, and returns the result. If you pass something that is not a context manager, you get a TypeError on 3.11+ (it was AttributeError before, so don’t write code that catches the wrong one).

stack.callback(fn, *args, **kw) registers a plain callable. It cannot suppress exceptions; if you need that, write an __exit__-shaped callable and use push.

stack.push(exit) is the more flexible form. It accepts either a context manager or any callable matching the (exc_type, exc, tb) -> bool shape, and it can suppress exceptions by returning True.

pop_all() moves the current callback stack to a fresh ExitStack and returns it. Use this when a library needs to hand cleanup responsibility off to its caller. After pop_all, the current stack no longer owns the callbacks; the new one does.

from contextlib import ExitStack

def acquire(): print("acquire")
def release(): print("release")

class Lock:
    def __enter__(self): print("lock"); return self
    def __exit__(self, *a): print("unlock")

with ExitStack() as stack:
    stack.callback(acquire, release)
    stack.push(Lock())  # if acquire succeeded, lock release goes on top
    print("work")
# output:
# acquire
# lock
# work
# unlock
# release

The most common surprise: callbacks do not run on garbage collection. If you skip with and forget to call stack.close(), the stack leaks. Always use with ExitStack() as stack: or call close() explicitly.

AsyncExitStack mirrors every method with async/await. It works with async with only; using it with a plain with is a TypeError.

ContextDecorator and AsyncContextDecorator

ContextDecorator is a mix-in class. Inherit from it and you can use your context manager both as a with block and as a function decorator.

from contextlib import ContextDecorator

class track_calls(ContextDecorator):
    def __enter__(self):
        print("enter")
        return self
    def __exit__(self, *exc):
        print("exit")
        return False

with track_calls():
    print("body")

# output:
# enter
# body
# exit

AsyncContextDecorator (3.10+) does the same for async. AsyncExitStack already inherits from it.

Abstract context manager base classes

AbstractContextManager and AbstractAsyncContextManager are ABCs that declare __enter__/__exit__ (or their async variants). AbstractContextManager.__enter__ already returns self by default, so subclasses only need to implement __exit__.

Their main modern use is typing. Use AbstractContextManager[T] to annotate a parameter that takes any sync context manager yielding T:

from contextlib import AbstractContextManager

def use(cm: AbstractContextManager[bytes]) -> bytes:
    with cm as data:
        return data

Both classes support __class_getitem__, so the square-bracket syntax is just regular generic syntax for the typing system, not metaprogramming.

Common mistakes

  • Using closing() on something that already supports with. closing(open(p)) is wrong; just use with open(p).
  • Yielding twice in a @contextmanager-decorated function. The second next() raises RuntimeError.
  • Swallowing exceptions inside a @contextmanager generator without re-raising. The with block exits cleanly even though the body raised.
  • Mixing with and async with. AsyncExitStack requires async with. Generators decorated with asynccontextmanager require async with.
  • Forgetting that pop_all() returns a stack that does not run on the original ExitStack. The original block’s __exit__ sees an empty stack and calls nothing.
  • Catching AttributeError on enter_context failures. It was that, but it has been TypeError since 3.11.
  • Skipping with and assuming ExitStack cleans up on garbage collection. It does not.

Conclusion

The contextlib module rewards learning it as a set of patterns, not as a list of names. @contextmanager covers 80% of ad-hoc needs. suppress, nullcontext, closing, and aclosing cover most of the rest. ExitStack and AsyncExitStack are the heavy artillery for dynamic resource sets. ContextDecorator and AbstractContextManager round out the typing and decorator ergonomics.

Pick the smallest tool that fits. Don’t reach for ExitStack when a single with will do, and don’t write a full class with __enter__/__exit__ when @contextmanager will work in five lines. Once the patterns click, the contextlib context manager utilities start to feel like a small, opinionated toolkit rather than a list of names to memorize.

Frequently asked questions

What’s the difference between @contextmanager and writing a class with __enter__/__exit__? For one-off resources, the decorator is shorter and clearer. For stateful resources, reused instances, or public APIs, a class is still the right call.

Is ExitStack slower than chaining with statements? Negligibly. The stack is just a list. Use it when the count is dynamic, not as a default replacement for with.

Can I nest with suppress(...) blocks? Yes. suppress is reentrant, so nested blocks compose cleanly without losing track of which exceptions belong to which level.

When should I reach for closing vs writing a class? closing is for objects with a .close() method but no __enter__/__exit__. If the object already supports with, do not wrap it.

Does redirect_stdout affect subprocesses? No. It mutates sys.stdout only. For subprocess output, capture at the file-descriptor level (for example, with os.dup2 or a temp file).

What about the contextlib async helpers like aclosing — when do they matter? Any time an async generator might break out early. Without aclosing, the generator’s finally and aclose() chain does not run, which leaks the underlying resource.

See also