Files and errorsChapter 86 of 114
Context Managers
Guarantee that cleanup happens, even when something goes wrong.
What with actually guarantees
with runs setup, then your block, then cleanup — and the cleanup runs whether the block finished, returned, or raised:
with open("notes.txt", "w", encoding="utf-8") as f:
f.write("one")
print(f.closed)Output
True
The alternative is a try/finally you have to remember to write:
f = open("notes.txt", "w", encoding="utf-8")
try:
f.write("one")
finally:
f.close()
print(f.closed)Output
True
with is that, in one line, with no way to forget the finally.
Cleanup happens even on an exception
try:
with open("notes.txt", "w", encoding="utf-8") as f:
f.write("partial")
raise ValueError("something went wrong")
except ValueError as problem:
print("caught:", problem)
print("file closed:", f.closed)Output
caught: something went wrong file closed: True
Writing one with a class
Two methods. __enter__ returns whatever the as name should be; __exit__ does the cleanup:
class Section:
def __init__(self, name):
self.name = name
def __enter__(self):
print("start", self.name)
return self
def __exit__(self, exc_type, exc, tb):
print("end", self.name)
return False
with Section("build") as section:
print("working on", section.name)Output
start build working on build end build
__exit__ receives the exception if there was one, and its return value decides whether to suppress it. Return False (or nothing) to let it continue. Returning True swallows it, which is almost never what you want:
class Quiet:
def __enter__(self):
return self
def __exit__(self, exc_type, exc, tb):
print("swallowing", exc_type.__name__)
return True
with Quiet():
raise ValueError("gone")
print("still running")Output
swallowing ValueError still running
The easier way: @contextmanager
For most cases a generator is shorter than a class. Everything before yield is setup, everything after is cleanup:
from contextlib import contextmanager
@contextmanager
def section(name):
print("start", name)
try:
yield name
finally:
print("end", name)
with section("build") as label:
print("working on", label)Output
start build working on build end build
The try/finally matters. Without it, an exception in the block would skip the cleanup:
from contextlib import contextmanager
@contextmanager
def section(name):
print("start", name)
try:
yield
finally:
print("end", name)
try:
with section("build"):
raise ValueError("failed")
except ValueError:
print("caught outside")Output
start build end build caught outside
A practical one
Temporarily changing something and putting it back is the classic use:
import os
from contextlib import contextmanager
@contextmanager
def env(name, value):
old = os.environ.get(name)
os.environ[name] = value
try:
yield
finally:
if old is None:
del os.environ[name]
else:
os.environ[name] = old
print(os.environ.get("MODE"))
with env("MODE", "test"):
print(os.environ.get("MODE"))
print(os.environ.get("MODE"))Output
None test None
Several at once
with open("a.txt", "w", encoding="utf-8") as a, open("b.txt", "w", encoding="utf-8") as b:
a.write("first")
b.write("second")
print(a.closed, b.closed)Output
True True
Both close, in reverse order, even if the second open fails.
suppress, for the exception you truly do not care about
import os
from contextlib import suppress
with suppress(FileNotFoundError):
os.remove("never-existed.txt")
print("carried on")Output
carried on
That is a tidier try/except/pass, and unlike a bare except it names exactly what it ignores.
Test yourself
2 questionsWhen does __exit__ run?
Show the answer
Always, including when the block raises — That guarantee is the whole reason with exists, and why you never need to remember a finally.
In a @contextmanager generator, why wrap the yield in try/finally?
Show the answer
Otherwise an exception in the block skips the cleanup — Everything before the yield is setup and everything after is cleanup, but only finally guarantees the cleanup runs.
Paths with pathlib
Build, inspect and search file paths without worrying about slashes.