Topic 9.8
Writing Context Managers with contextlib
In one line
contextlib lets you write your own with blocks using a generator and @contextmanager, and gives tools for common jobs: suppress to ignore an error, ExitStack to manage a changing number of resources, closing, redirect_stdout and nullcontext.
Think of it like this
Borrowing a library book. You check it out (setup), read it (the work), and the book must go back, even if you are interrupted or lose interest halfway (cleanup). A context manager guarantees the "go back" part. The with statement is the library desk: whatever happens inside, the cleanup runs.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Context manager
- An object that does setup when a
withblock starts and cleanup when it ends. - with statement
- Runs a block between the context manager's setup and cleanup.
- __enter__ / __exit__
- The two special methods behind
with: one at the start, one at the end. - @contextmanager
- A decorator that turns a one-yield generator function into a context manager.
- try / finally
- Code in
finallyruns whatever happens intry, even after an exception (Topic 7.1). - ExitStack
- A container that can hold any number of context managers and clean them up in reverse order.
- suppress
- A context manager that quietly ignores the named exception types.
- Buffer (StringIO)
- An in-memory text object that behaves like a file.
Step by step
01The with statement, spelled out
with A() as x: body does roughly this: call A().__enter__() and give the result to x; run body inside a try; whatever happens, call __exit__. If __exit__ returns a true value, an exception from the body is swallowed; otherwise it continues up.
02A generator-based context manager
announce prints a start line, yields a value, and prints an end line in finally. Because of finally, the end line prints even when the body raises an error.
from contextlib import contextmanager
@contextmanager
def announce(name):
print(f"start {name}")
try:
yield name.upper()
finally:
print(f"end {name}")
with announce("job") as label:
print("working on", label)03Why try/finally matters
If the body raises, the exception is thrown into the generator **at the yield**. Without try/finally, it ends the generator right there and the cleanup code is skipped. The error then continues upward. The break-it example runs this without finally so you can see the "end" line missing.
04suppress and nullcontext
with suppress(FileNotFoundError): open("missing.txt") replaces four lines of try/except/pass. Only the listed errors are ignored; other errors still raise. nullcontext() is a with that does nothing, useful when a resource is optional.
05ExitStack for a dynamic number of resources
You cannot write with for a list of files whose length you do not know. ExitStack solves it: enter each manager into the stack, and they are all closed together at the end, last opened first closed. stack.callback(func, *args) registers an arbitrary cleanup call.
06closing and redirect_stdout
closing(obj) calls obj.close() on exit for old-style objects. redirect_stdout(buffer) temporarily replaces sys.stdout, so every print inside the block goes into the buffer. Afterwards buffer.getvalue() has the captured text. The original stdout is restored even after an error.
07Class form versus generator form
Use @contextmanager for simple setup and cleanup. Write a class with __enter__ and __exit__ when the context manager needs to be reusable several times, needs extra methods, or must decide itself whether to swallow an exception. Since Python 3.2, a context manager made with @contextmanager can also be used as a decorator on functions.
Try it yourself
- 1
Time a block without printing the time
Write a
@contextmanagernamedtimed(label)that printsstart label, yields, and printsdone labelinfinally. Use it around a loop. In real code you would recordtime.perf_counter()before and after and log the difference; here we print fixed text so the output never changes. - 2
Open three files at once
Extend the ExitStack example to write and read three files. Check that
all(f.closed ...)is stillTrue.
Code & diagrams
Remove the try/finally in announce and run again. The 'end risky' line disappears when the body fails.
Expected output
start job
working on JOB
end job
start risky
end risky
caught: boom
suppress swallowed the error
processing
start locked
processing
end lockedAdd a third file name to the list. ExitStack closes however many you open.
Expected output
['one', 'two']
cleanup callback ran
all closed: True
using connection
connection closed
captured: 'this goes into the buffer\n'Databases and payment code use exactly this shape: change things inside the block, keep the changes if it finishes, undo them if it fails. The except branch undoes the change and then re-raises, so the caller still sees the error.
Expected output
committed
after success: {'balance': 70}
rolled back because: insufficient funds
after failure: {'balance': 70}Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Cleanup without try/finally
Write the cleanup after the yield but without a try/finally.
from contextlib import contextmanager
@contextmanager
def announce(name):
print(f"start {name}")
yield name
print(f"end {name}")
try:
with announce("risky"):
raise ValueError("boom")
except ValueError:
print("caught")Myth vs fact
Myth
with only works for files.
Fact
It works for anything with __enter__ and __exit__: locks, database transactions, temporary directories, network connections, and your own context managers.
Myth
Code after the yield in a @contextmanager always runs.
Fact
Only if you put it in finally. If the body raises and there is no try/finally, it is skipped.
Myth
suppress hides all errors.
Fact
It ignores only the exception types you name. Others propagate normally.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
@contextmanageris built on the generator methods from Topic 9.2: on entry it callsnext()on the generator; on exit with no error it callsnext()again and expectsStopIteration; on exit with an error it callsgen.throw(error)so the exception appears at theyield. If the generator yields a second time, you getRuntimeError: generator didn't stop. The object that@contextmanagerreturns is single-use: entering the same one twice fails (call the function again for a fresh one). Calling it with@mgr()as a decorator, however, builds a fresh one for each call. Also: if__enter__itself raises,__exit__is not called, so put acquisition that can fail before thetry, and cleanup only for what was really acquired. - ▸
If your cleanup code catches the exception and does not re-raise it,
@contextmanagerswallows it. To swallow deliberately, catch it around theyieldand do not raise; otherwise let it propagate (or usefinally, which does not swallow). - ▸
Python 3.10 added parenthesised
withitems, so several managers can be written across lines in one statement. For a runtime-chosen number,ExitStackis still the tool, andAsyncExitStackwith@asynccontextmanagerdoes the same forasync with(Phase 12). Python 3.11 addedcontextlib.chdir(path)for a temporary change of working directory, andaclosingis the async version ofclosing. With several managers in onewith, they exit in reverse order, and if two exits raise, the later error is chained to the earlier. - ▸
In AI and data services, context managers guard transactions (commit on success, roll back on error), temporary configuration changes (restore the old value in
finally), and timing or tracing spans around each call to a model.
Remember this
- 1
You met context managers as a user in Topic 7.4:
with open(path) as f:. Thewithstatement calls__enter__at the start, runs the block, and calls__exit__at the end, even if an exception happened. Writing a class with those two methods works, butcontextlib.contextmanagerdoes it in a few lines with a generator. - 2
Recall from Topic 9.2: a generator pauses at each
yieldand resumes later.@contextmanagerturns a generator function with **exactly oneyield** into a context manager. Code before theyieldis the setup (__enter__). The value yielded is whatasreceives. Code after theyieldis the cleanup (__exit__). So a generator pauses at theyieldwhile thewithbody runs, then continues afterwards. - 3
Put the
yieldinsidetry:and the cleanup infinally:. Withouttry/finally, an exception in thewithbody is raised at the yield, the generator ends early, and the cleanup lines after theyieldnever run. This is the most important rule of this topic, and the break-it example shows it. - 4
suppress(ErrorType)ignores the named errors inside the block:with suppress(FileNotFoundError): os.remove(path). It reads better than an emptytry/except.nullcontext()does nothing: it stands in when a context manager is optional (with lock or nullcontext():).closing(obj)callsobj.close()at the end for objects that have aclosemethod but are not context managers themselves. - 5
ExitStackmanages a changing number of context managers or cleanup steps. You open files in a loop withstack.enter_context(open(name)), register extra cleanup withstack.callback(func, ...), and when thewith ExitStack()block ends, everything is closed in the reverse order, even if one of them fails. Use it when you cannot write a fixed number ofwithitems. - 6
redirect_stdout(target)sendsprintoutput into a buffer such asio.StringIO(), which is useful in tests and for capturing what a noisy function prints. In AI and data code, custom context managers are common for timing a block, holding a database transaction, temporarily changing a setting, or opening and closing a connection to a model service or vector database.
Explain it without notes
What does the with statement guarantee, and how do __enter__ and __exit__ fit in?
How does @contextmanager turn a generator into a context manager, and what must you do around the yield?
When would you use ExitStack?
What do suppress, nullcontext and closing each do?
Practice
Write a @contextmanager named banner(title) that prints === title === before the block and === end === after it (in finally). Use it around one print.
Use suppress so that deleting a missing key from a dictionary does not raise: del data["zzz"], then print data.
Write a @contextmanager named temporary_value(box, key, value) that sets box[key] = value for the block and restores the old value afterwards. Show the dictionary before, inside and after.
Trade-offs
- ↔
Short generator form versus a full class.
@contextmanageris short and clear for one-off setup/cleanup, but a class is better when the manager is reused, has extra methods or needs fine control over exceptions. - ↔
Guaranteed cleanup versus hidden control flow.
withmakes cleanup certain, but a context manager that swallows exceptions can hide bugs. Swallow errors only on purpose, and name the manager so that is obvious (suppress).
Done when you can
Done when you can explain what
__enter__and__exit__do in awithblock.Done when you can write a context manager with
@contextmanagerusingtry/finally.Done when you can explain why the cleanup is skipped without
finally.Done when you can use
suppress,nullcontextandclosingfor their jobs.Done when you can use
ExitStackto open a list of files.