with exists so that cleanup happens whatever the body does. Return early, raise, or finish normally, and the exit code still runs.
Any object with __enter__ and __exit__ can be used this way, so you can put your own resources behind the same statement.
What the two methods do
__enter__ runs first and its return value is what as binds. __exit__ runs at the end and is told which exception, if any, is in flight.
# what with actually guarantees
import io
class Tracked:
def __init__(self):
self.events = []
def __enter__(self):
self.events.append('enter')
return self
def __exit__(self, exc_type, exc, tb):
self.events.append(f'exit ({exc_type.__name__ if exc_type else "no error"})')
return False
t = Tracked()
with t as handle:
handle.events.append('body')
print(t.events)
It prints:
['enter', 'body', 'exit (no error)']
Cleanup runs on the way out of an error
The exception carries on after __exit__ has run.
# cleanup runs even when the body raises
t2 = Tracked()
try:
with t2:
t2.events.append('body')
raise ValueError('boom')
except ValueError as err:
t2.events.append(f'caught {err}')
print(t2.events)
It prints:
['enter', 'body', 'exit (ValueError)', 'caught boom']
This is the whole point. A try/finally would do the same, but with puts the cleanup next to the resource instead of at every call site.
Returning True swallows the exception
A true return value from __exit__ means “I handled it”.
# returning True from __exit__ swallows the exception
class Swallow:
def __enter__(self):
return self
def __exit__(self, exc_type, exc, tb):
return True
with Swallow():
raise RuntimeError('this never escapes')
print('still running')
It prints:
still running
Use this rarely and deliberately. Silently swallowing every exception is how errors go missing.
The contextlib shortcut
A generator with a single yield becomes a context manager. Everything before the yield is setup, everything after is cleanup.
# contextlib turns a generator into a context manager
import contextlib
@contextlib.contextmanager
def tag(name, out):
out.append(f'<{name}>')
try:
yield out
finally:
out.append(f'</{name}>')
lines = []
with tag('p', lines):
lines.append('hello')
print(''.join(lines))
It prints:
<p>hello</p>
The try/finally around the yield is what makes cleanup run when the body raises. Without it, an exception skips the rest of the generator.
suppress instead of try/except/pass
contextlib.suppress says what it means and takes one line.
# contextlib.suppress instead of try/except/pass
with contextlib.suppress(ZeroDivisionError):
1 / 0
print('suppressed')
It prints:
suppressed
Several managers at once
Commas nest them left to right, and they close in reverse order.
# several managers on one line
buf1, buf2 = io.StringIO(), io.StringIO()
with buf1 as a, buf2 as b:
a.write('one')
b.write('two')
print(a.getvalue(), b.getvalue()) # read before the block closes them
print('both closed:', buf1.closed, buf2.closed)
It prints:
one two
both closed: True True
Note the second line. Once the block ends, the files are closed, so read what you need inside.
What to remember
__enter__returns whatasbinds,__exit__always runs.- A true return from
__exit__swallows the exception. @contextlib.contextmanagerplustry/finallyis the short way to write one.- Managers on one
withline close in reverse order.
Locks, database transactions, temporary directories, timers and mocked settings are all context managers in the standard library. If your code has a setup and a matching teardown, it wants to be one too.