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 supportswith.closing(open(p))is wrong; just usewith open(p). - Yielding twice in a
@contextmanager-decorated function. The secondnext()raisesRuntimeError. - Swallowing exceptions inside a
@contextmanagergenerator without re-raising. Thewithblock exits cleanly even though the body raised. - Mixing
withandasync with.AsyncExitStackrequiresasync with. Generators decorated withasynccontextmanagerrequireasync with. - Forgetting that
pop_all()returns a stack that does not run on the originalExitStack. The original block’s__exit__sees an empty stack and calls nothing. - Catching
AttributeErroronenter_contextfailures. It was that, but it has beenTypeErrorsince 3.11. - Skipping
withand assumingExitStackcleans 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.