Context Managers (with)
Whenever code acquires a resource — a file handle, a network socket, a database
connection, a lock — that resource needs to be released afterwards, no matter what
happens in between. If an exception is raised halfway through, cleanup still has to run.
Python's with statement exists to make that guarantee automatic, replacing hand-written
try/finally boilerplate with a clean, declarative block.
Why with exists: the manual way vs. with
Before context managers, guaranteeing cleanup meant wrapping every risky operation
in try/finally by hand. It works, but it's easy to forget, and it gets repetitive
fast.
# Before: manual try/finally cleanup
f = open('notes.txt', 'w')
try:
f.write('Hello, file!')
f.write('\nSecond line.')
finally:
f.close() # must remember this, even if write() raisesThe with statement does the exact same thing — open, use, guarantee close — but the
cleanup is baked into the object itself, so you can't forget it.
# After: the with statement
with open('notes.txt', 'w') as f:
f.write('Hello, file!')
f.write('\nSecond line.')
# f.close() has already been called here, automatically —
# even if an exception had been raised inside the blockThe classic example: reading a file
The most common use of with is file handling. Opening a file inside a with block
guarantees the file is closed as soon as the block ends, whether it finished normally,
returned early, or raised an exception.
with open('notes.txt', 'r') as f:
contents = f.read()
print(contents)
# f.closed is True out here — the file was closed automatically
print(f.closed)Hello, file! Second line. True
Writing a custom context manager (a class)
Any object can be used with with as long as it implements two special methods:
__enter__ and __exit__. __enter__ runs when the block is entered and its return
value is what gets bound to the as variable. __exit__ runs when the block ends —
whether normally or via an exception — and is where cleanup belongs.
import time
class Timer:
"""Context manager that measures how long a block takes to run."""
def __enter__(self):
self.start = time.perf_counter()
return self # this becomes the 'as' variable
def __exit__(self, exc_type, exc_value, traceback):
self.elapsed = time.perf_counter() - self.start
print(f'Elapsed: {self.elapsed:.4f}s')
return False # do not suppress exceptionswith Timer() as t:
total = sum(i * i for i in range(1_000_000))
print('Sum computed:', total)Elapsed: 0.0821s Sum computed: 333332833333500
__exit__ always receives three arguments describing any exception that occurred
inside the block: exc_type, exc_value, and traceback. If the block completed
without error, all three are None. If __exit__ returns a truthy value (like
True), Python treats the exception as handled and suppresses it — the with block
exits quietly instead of propagating the error. Returning False (or nothing, since
None is falsy) lets the exception continue propagating normally, which is almost
always what you want.
The simpler way: @contextmanager
Writing a full class with __enter__ and __exit__ is verbose for simple cases.
The contextlib.contextmanager decorator lets you write a context manager as a single
generator function instead: code before yield is the setup, code after yield (in a
finally block) is the cleanup.
from contextlib import contextmanager
import time
@contextmanager
def timer():
start = time.perf_counter()
try:
yield # control passes to the with-block here
finally:
elapsed = time.perf_counter() - start
print(f'Elapsed: {elapsed:.4f}s')with timer():
total = sum(i * i for i in range(1_000_000))
print('Sum computed:', total)This produces the same behavior as the Timer class above, but with far less code.
The try/finally around yield is what guarantees the cleanup code still runs even
if the with block raises an exception — the exception is raised at the yield point
inside the generator, so finally always executes.
Multiple context managers in one statement
When you need more than one resource at once — copying from one file to another, for
example — you can combine multiple context managers in a single with statement using
commas. This is equivalent to nesting them, but reads more clearly.
with open('source.txt', 'r') as src, open('destination.txt', 'w') as dst:
dst.write(src.read())
# both files are closed automatically here, in reverse order of openingSummary
withguarantees cleanup code runs, even when exceptions occur — no more forgetting afinallyAny class implementing
__enter__and__exit__can be used withwith__exit__(self, exc_type, exc_value, traceback)can inspect or suppress exceptions by returning a truthy value@contextmanagerfromcontextlibturns a generator function into a context manager usingyieldMultiple resources can share one
withstatement using commas