The with statement guarantees that setup runs before a block and cleanup runs after it - even if the block raises. Python's open() is the one you meet first, but the same protocol handles locks, database transactions, temporary directories and timing blocks.
What does the with statement actually do?
Given an object with __enter__ and __exit__ methods, with calls __enter__, binds its return value to the as name, runs the block, and then calls __exit__ - with the exception information if something went wrong, and without it if everything was fine.
from time import perf_counter
class Timer:
"""Measure how long the block takes."""
def __enter__(self) -> "Timer":
self.start = perf_counter()
return self
def __exit__(self, exc_type, exc, tb) -> bool:
self.elapsed = perf_counter() - self.start
print(f"block took {self.elapsed:.6f}s")
return False
with Timer() as timer:
sum(range(100000))
print(f"elapsed recorded: {timer.elapsed:.6f}s")
Two things are worth noticing. The value returned by __enter__ is what as binds - returning self is the convention that lets the object carry state out of the block. And __exit__ returning False means "do not suppress the exception"; returning True would swallow it, which is a powerful tool and a good way to hide bugs.
The three arguments __exit__ receives are the exception type, the exception instance and the traceback - exactly what sys.exc_info() would return inside an except block. When the block finishes cleanly, all three are None, which is how a manager knows that nothing went wrong.
The with block is also exception-safe. If the body raises, __exit__ still runs, so the cleanup cannot be skipped by an error - which is exactly the guarantee a try/finally block gives you, with less noise.
How do you write a context manager without a class?
The contextlib.contextmanager decorator turns a generator function into a context manager. The code before the yield is setup, the yield hands the value to the with statement, and the code after it is cleanup.
from contextlib import contextmanager
@contextmanager
def open_text(path: str, mode: str = "r"):
"""Open a file and close it when the block ends."""
handle = open(path, mode, encoding="utf-8")
try:
yield handle
finally:
handle.close()
with open_text("notes.txt", "w") as handle:
handle.write("first line\nsecond line\n")
with open_text("notes.txt") as handle:
print(handle.read().strip())
The try/finally around the yield is not optional decoration. If the block raises, the generator is thrown into at the yield point; the finally clause is what closes the file anyway. Without it, the file stays open until the garbage collector gets round to it.
The pattern generalises to any resource with an acquire step and a release step - a lock, a temporary directory, a database transaction.
Which context managers does the standard library give you?
You do not have to write most of these yourself. contextlib covers the common shapes, and several of them are worth reaching for by name.
import io
from contextlib import nullcontext, redirect_stdout, suppress
buffer = io.StringIO()
with redirect_stdout(buffer):
print("captured output")
print("buffer holds:", buffer.getvalue().strip())
with suppress(ZeroDivisionError):
1 / 0
print("the error was suppressed")
with nullcontext("default") as value:
print("nullcontext gave:", value)
redirect_stdout captures printed output, which is how test frameworks assert on what a function printed. suppress swallows one named exception type and lets everything else through. nullcontext does nothing at all, which is useful when a context manager is optional and you need to branch between "with one" and "without one".
Two more are worth knowing. contextlib.closing turns any object with a close() method into a context manager, and contextlib.chdir changes the working directory for the length of the block - available since Python 3.11.
from contextlib import ExitStack, closing
class Resource:
"""A resource that must be closed."""
def __init__(self, name: str) -> None:
self.name = name
self.closed = False
def close(self) -> None:
self.closed = True
print(f"closed {self.name}")
with closing(Resource("report")) as resource:
print("using", resource.name)
print("closed already:", resource.closed)
ExitStack is the general-purpose version: you register resources as you discover them and the stack closes them all in reverse order when the block ends, even if one of the closes raises. It is how libraries that open an unknown number of files keep their cleanup correct.
How do you suppress an exception, and when should you?
There are two mechanisms. Returning True from __exit__ swallows any exception that matches your condition, and contextlib.suppress does the same thing for named exception types.
class IgnoreValueError:
"""Swallow ValueError and let everything else through."""
def __enter__(self):
return self
def __exit__(self, exc_type, exc, tb):
return exc_type is ValueError
with IgnoreValueError():
raise ValueError("this is ignored")
print("execution continued")
try:
with IgnoreValueError():
raise KeyError("this one is not ignored")
except KeyError as error:
print("not ignored, so it reached the caller:", error)
The first block swallows the ValueError and execution continues. The second raises a KeyError, which the same context manager does not match, so it propagates out of the with statement and is caught by the surrounding try - the traceback ends with KeyError: 'this one is not ignored'.
Suppress deliberately or not at all. Swallowing an exception usually removes the evidence of a bug, and a program that continues with bad state fails later in a place that is much harder to diagnose. The good uses are narrow and specific: an optional cleanup step that is allowed to fail, or a lookup that has a sensible default.
How do you manage several resources at once?
One with statement can hold several managers, separated by commas. Python enters them left to right and exits them right to left, which is the order you want when the resources depend on each other.
from contextlib import contextmanager
@contextmanager
def open_text(path: str, mode: str = "r"):
"""Open a file and close it when the block ends."""
handle = open(path, mode, encoding="utf-8")
try:
yield handle
finally:
handle.close()
with open_text("input.txt", "w") as source, open_text("copy.txt", "w") as target:
source.write("payload\n")
target.write("payload\n")
with open_text("copy.txt") as handle:
print("copied contents:", handle.read().strip())
For a number of resources that is not known in advance, use ExitStack and register each one with stack.enter_context(...). The stack unwinds everything for you when the block ends.
The ordering rule is worth remembering because it appears in every language with a with statement: resources are acquired left to right and released right to left. If handle B depends on handle A, write with a as first, b as second - the reverse order would close A while B is still using it.
What to remember
withcalls__enter__, runs the block, then always calls__exit__.- Use
@contextmanagerand atry/finallyaround theyieldfor generator-based managers. contextlibshipssuppress,nullcontext,redirect_stdout,closingandExitStack.- Returning
Truefrom__exit__suppresses the exception - use it deliberately. - Several managers can share one
withstatement; they exit in reverse order.
Try it yourself: Write a @contextmanager named temporary_file(prefix) that creates a file with tempfile.NamedTemporaryFile(delete=False), yields the path and deletes the file when the block ends - even if the block raises. Use it to write a report to disk, then read the file back inside the same block. Add a second context manager of your own that restores the current working directory with os.getcwd() and os.chdir(), and combine both in one with statement.