AcademyPython
Context Managers in Python
What with is really calling, why __exit__ runs even when the block raises, and how @contextmanager turns a generator into one without writing a class.
3 min readUpdated 3 exercises
What with is syntax for
with open('data.txt') as f:
contents = f.read()is equivalent to:
f = open('data.txt').__enter__()
try:
contents = f.read()
finally:
open('data.txt').__exit__(...)— except the object is only constructed once, and __enter__ and __exit__ are called on that same object. __enter__ runs before the block and its return value is what as f binds; __exit__ runs after the block ends, whether it ended normally or by raising. That guarantee — cleanup runs no matter how the block exits — is the entire reason the protocol exists: f.close() in a finally clause does the same thing, but with makes forgetting the finally impossible.
The two methods a context manager needs
class Timer:
def __enter__(self):
self.start = time.perf_counter()
return self
def __exit__(self, exc_type, exc_value, traceback):
self.elapsed = time.perf_counter() - self.start
return False
with Timer() as t:
do_work()
print(t.elapsed)__exit__ receives the exception type, value and traceback if the block raised, or three Nones if it did not. Returning False — or nothing, since None is falsy — lets a raised exception propagate normally after cleanup runs. Returning True swallows it, which is occasionally intentional but easy to do by accident: a bare return with no value at the end of __exit__ is fine, but a stray return True silently deletes every exception the block could raise.
Writing one without a class
from contextlib import contextmanager
@contextmanager
def timer():
start = time.perf_counter()
try:
yield
finally:
print(f'took {time.perf_counter() - start:.3f}s')
with timer():
do_work()@contextmanager turns a generator function into a context manager: everything before yield is __enter__, the yielded value is what as binds, and everything after yield is __exit__. Wrapping the resume in try/finally is what makes the cleanup run even if the block raises — the exception is thrown back into the generator at the yield point, and without the finally the cleanup code below yield would simply never run.
Why open() is the example everyone reaches for
A file handle is a scarce resource with an unambiguous cleanup step, which is exactly the case the protocol is for: acquire something, guarantee its release, regardless of what happens in between. The same shape covers a database transaction that must commit or roll back, a lock that must release, a temporary directory that must be deleted, or a monkey-patched setting that must be restored — anything with a clear "undo" step belongs behind a context manager rather than a manual try/finally repeated at every call site.
Nesting and combining several
with open('in.txt') as src, open('out.txt', 'w') as dst:
dst.write(src.read())A single with statement can open more than one context manager, entering them left to right and exiting them in reverse order — the same nesting a stack of individual with blocks would produce, without the indentation. contextlib.ExitStack handles the case where the number of context managers is not known until runtime, entering each one dynamically and still guaranteeing every one of them exits, even if a later one in the stack raises during setup.
Now practice it
1 challenge · 2 quizzesReading this page is the cheap half. These are the exercises that make you use it.
- Error Handling & Context Managers · Chaining & Context ManagersCovers the __enter__/__exit__ protocol directly, including how exceptions cross a with block.QuizTake · MediumQuizTake · Medium
- Error Handling & Context Managers · ExitStack & Exception GroupsExitStack and the harder edges of combining several context managers at once.QuizTake · HardQuizTake · Hard
- Context Manager — TimerProBuild a timing context manager from scratch — the exact Timer shape this page walks through.ChallengeSolve · HardChallengeSolve · Hard
Read next
Exceptions and Error Handling in Python
What each clause of try/except/else/finally actually guarantees, how raise from chains a new exception to its cause, and why a bare except is almost always wrong.
3 min
The GIL and Concurrency in Python
Why threading does not speed up CPU-bound Python code, what the GIL actually serialises, and when to reach for multiprocessing or asyncio instead.
3 min