Lesson 5 of 10
with open(...) from lesson 6.2 guarantees the file closes. That guarantee comes from the
context manager protocol, and you can implement it on your own objects.
Any object with __enter__ and __exit__ works in a with block.
The protocol
class Greeter:
def __enter__(self):
print("entering")
return "the value bound by as"
def __exit__(self, exc_type, exc_value, traceback):
print("exiting")
with Greeter() as value:
print(value)
__enter__runs at the start. Whatever it returns is bound byas.__exit__runs at the end — guaranteed, exception or not.
Guaranteed cleanup
That guarantee is the whole point:
class Resource:
def __enter__(self):
print("acquired")
return self
def __exit__(self, exc_type, exc_value, traceback):
print("released")
try:
with Resource():
print("working")
raise ValueError("something broke")
except ValueError as e:
print(f"caught: {e}")
"released" prints even though the block raised. Same idea as finally in lesson 6.6, but
reusable and named.
The __exit__ arguments
Those three parameters describe the exception, or are all None if the block finished cleanly:
class Watcher:
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, traceback):
if exc_type is None:
print("clean exit")
else:
print(f"exited with {exc_type.__name__}: {exc_value}")
with Watcher():
pass
try:
with Watcher():
raise ValueError("oops")
except ValueError:
print("propagated")
Returning True from __exit__ suppresses the exception. Return None (or nothing) to let
it propagate — which is almost always what you want. Silently swallowing errors is the trap from
lesson 6.6 in a new costume.
class Suppressor:
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, traceback):
return True
with Suppressor():
raise ValueError("this vanishes")
print("still running")
contextlib.contextmanager
The class is verbose for simple cases. The @contextmanager decorator turns a generator into a
context manager — everything before yield is setup, everything after is teardown:
from contextlib import contextmanager
@contextmanager
def greeting():
print("before")
yield "hello"
print("after")
with greeting() as value:
print(value)
For guaranteed cleanup you need try/finally, or an exception skips the teardown:
from contextlib import contextmanager
@contextmanager
def resource():
print("acquired")
try:
yield "handle"
finally:
print("released")
try:
with resource() as r:
print(f"using {r}")
raise ValueError("broke")
except ValueError:
print("caught")
A practical example
A timer, using the pattern for something you'd genuinely write:
from contextlib import contextmanager
@contextmanager
def tracked(label):
"""Report entry and exit around a block of work."""
print(f"[{label}] start")
try:
yield
finally:
print(f"[{label}] done")
with tracked("import"):
total = sum(range(100))
print(f"computed {total}")
Swap the prints for time.perf_counter() and you have a profiler.
Try it yourself
Checkpoint
__enter__returns theasvalue;__exit__always runs.__exit__receives exception details, or threeNones on a clean exit.- Returning
Truefrom__exit__swallows the exception — rarely what you want. @contextmanagerturns a generator into a context manager: setup,yield, teardown.- Wrap the
yieldintry/finallyso cleanup survives exceptions.
Questions about this lesson
Log in to ask a question.