The receipt printer jams halfway through a write, and the file stays open. A with block promises cleanup even when the body fails. What exactly does the context manager guarantee?
A with block runs setup, executes a body, and guarantees cleanup even when the body raises an exception. Files are the everyday example, but the same protocol works for locks, temporary state, database transactions, and test fixtures. You will also suppress one expected error, clean up on failure, and manage a dynamic number of resources.
Let with close the file
The with statement calls the file's context manager methods for you. The file is closed when the block ends, including when an exception escapes. You do not need a separate close call on every path.
from pathlib import Path
path = Path("pypractice-context.txt")
path.write_text("hello\n", encoding="utf-8")
with path.open(encoding="utf-8") as handle:
print(handle.read().strip())
path.unlink()Automatic cleanup
Understand enter and exit
A context manager implements __enter__, which prepares the resource, and __exit__, which receives exception details and performs cleanup. A with statement calls them around the body. Returning a false value from __exit__ lets an exception propagate.
class Guard:
def __enter__(self):
print("enter")
return self
def __exit__(self, exc_type, exc, tb):
print("exit")
with Guard():
print("inside")The context manager protocol
Build one from a generator
contextlib.contextmanager turns a generator into a context manager. Everything before yield is setup, and everything after yield is cleanup. The value yielded becomes the value bound by as.
from contextlib import contextmanager
@contextmanager
def label(name):
print("start", name)
yield name
print("end", name)
with label("demo") as value:
print("body", value)A compact custom manager
__exit__ can suppress one error
Returning True from __exit__ suppresses the exception. Use that power only for a narrow, expected failure, such as a missing optional file. Returning True for every exception hides bugs; contextlib.suppress expresses the same intent with less code.
class IgnoreMissing:
def __enter__(self):
return self
def __exit__(self, exc_type, exc, tb):
if exc_type is FileNotFoundError:
print("ignored missing file")
return True
return False
with IgnoreMissing():
raise FileNotFoundError("demo")
print("after")Suppress only the expected type
Cleanup runs when the body fails
A contextmanager function should put cleanup in finally around yield. The code after yield runs on normal exit and on exception, but only if control returns through the generator. Catching the exception outside the with block keeps cleanup separate from error handling.
from contextlib import contextmanager
@contextmanager
def session():
print("open")
try:
yield "ready"
finally:
print("close")
try:
with session() as state:
print(state)
raise RuntimeError("boom")
except RuntimeError as error:
print("caught", error)yield inside try, cleanup in finally
ExitStack manages a dynamic list
When the number of resources is not known until runtime, ExitStack can register each one. It closes them in reverse order when the block exits. This avoids writing a variable-depth nest of with statements.
from contextlib import ExitStack
from pathlib import Path
from tempfile import TemporaryDirectory
with TemporaryDirectory() as tmp:
root = Path(tmp)
with ExitStack() as stack:
handles = []
for name in ["a.txt", "b.txt"]:
handle = stack.enter_context((root / name).open("w", encoding="utf-8"))
handles.append(handle)
handle.write(name)
print(len(handles), "open")
print("all closed")Register resources as you create them
Key points
- with guarantees cleanup when the block exits.
- A context manager implements __enter__ and __exit__.
- __exit__ receives exception information and can suppress it.
- contextlib.contextmanager builds a manager from a generator with one yield.
- Suppress narrowly; put cleanup in finally; use ExitStack for dynamic resources.
Common mistakes
- Opening a file without a with block and forgetting to close it on every path.
- Thinking __exit__ runs only when there is no exception.
- Forgetting that the code after yield in a contextmanager runs during cleanup.
- Returning True from __exit__ for every exception and hiding real bugs.
Open a small file with with, read it, and delete it after the block.
Try it yourself
Change the values, predict the output, then run the code. The Python runtime is downloaded the first time you press Run, so the first run can take a few seconds.
Practice this lesson
12 questions with immediate explanations.