Python With Statements and Context Managers
Learn Python context managers, entry and cleanup, exception propagation, multiple resources, and contextlib.suppress(), nullcontext(), and closing().
Intermediate
A context manager controls a runtime context around a block of code. Python's with statement enters that context before the block and exits it afterward, making setup and cleanup easier to keep together.
Start with with
The with statement takes a context manager and runs a block inside its context. The optional as target receives the value returned by __enter__().
class Session:
def __enter__(self):
print("start")
return "ready"
def __exit__(self, exc_type, exc_value, traceback):
print("finish")
with Session() as state:
print(state)The object after with is the context manager. The name after as is not assigned that object automatically; it receives the value returned by __enter__().
__enter__ prepares the context
A context manager's __enter__ method runs before the with block. It can establish the runtime context and return a useful value for the as target.
class Counter:
def __enter__(self):
self.count = 0
print("entered")
return self
def __exit__(self, exc_type, exc_value, traceback):
print("exiting")
with Counter() as counter:
counter.count += 1
print(counter.count)__exit__ always handles normal block exit
If __enter__ completes successfully, Python calls __exit__ when the with block finishes. When the block ends normally, the exception arguments passed to __exit__ are all None.
class TimerLabel:
def __enter__(self):
print("begin")
return self
def __exit__(self, exc_type, exc_value, traceback):
print("end", exc_type is None)
with TimerLabel():
print("work")__exit__ also runs when the block raises
The cleanup part is the important guarantee: after __enter__ succeeds, __exit__ is called even when the with block raises an exception. If __exit__ returns a false value, the exception continues propagating.
class Cleanup:
def __enter__(self):
print("open")
return self
def __exit__(self, exc_type, exc_value, traceback):
print("cleanup", exc_type.__name__)
return False
try:
with Cleanup():
raise ValueError("bad data")
except ValueError as error:
print("handled", error)An __exit__ method can suppress an exception
If __exit__ returns a true value after an exception, the with statement suppresses that exception and execution continues after the with statement. This should be used deliberately because it changes normal error propagation.
class IgnoreValueError:
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, traceback):
return exc_type is ValueError
with IgnoreValueError():
raise ValueError("ignored")
print("continues")Suppress a specific expected exception with contextlib.suppress
The standard-library contextlib.suppress() helper can replace a small try/except block when one specific exception is an expected and harmless outcome. It suppresses only the exception types listed in the call; other exception types still propagate. Unlike the previous example, you do not need to write a custom __exit__ method just to express this policy.
Output
Ready to run.
Quick Check: keep exception suppression narrow
The program should ignore a missing dictionary key but let unrelated errors propagate. Which context manager matches that intent?
from contextlib import suppress
settings = {"theme": "dark"}
with suppress(KeyError):
settings.pop("language")
print("theme:", settings["theme"])
Keep the with block small. suppress(KeyError) handles a KeyError raised anywhere inside that block, not only the line you intended to guard. Do not use suppress(Exception) or suppress(BaseException) as a general-purpose error-handling strategy; catch and report failures that are not known to be harmless.
Use nullcontext when a resource is already managed elsewhere
contextlib.nullcontext(value) gives you a context manager that returns value from __enter__() and otherwise does nothing. It is useful when one function accepts either a path it should open and close itself or an already-open stream whose caller keeps ownership. In the path case, use open() as the context manager; in the borrowed-stream case, use nullcontext(stream). The with statement then has one consistent shape without accidentally closing the caller’s stream.
Output
Ready to run.
Quick Check: preserve the caller’s resource
Which expression binds active to the existing stream without closing it when the with block ends?
from contextlib import nullcontext
from io import StringIO
stream = StringIO("caller-owned\n")
with as active:
print(type(active).__name__)
print("same object:", active is stream)
print("stream closed:", stream.closed)
nullcontext() is not a cleanup helper: it does not call close() or suppress exceptions. It simply provides the same with/as structure when no context-manager work is needed. Use it when ownership should remain with the caller, and use a real resource context manager when the current function owns the resource.
Close objects that do not implement the context-manager protocol
Some legacy or third-party objects provide close() but do not implement __enter__() and __exit__(), so they cannot be used directly in a with statement. contextlib.closing(object) adapts such an object: it returns the object from __enter__() and calls its close() method when the block exits, including when the block raises an exception. Prefer an object’s native context-manager support when it has one; use closing() when close() is its cleanup interface and this scope is responsible for closing it.
Output
Ready to run.
Quick Check: close a legacy resource
A resource has close() but does not implement __enter__() or __exit__(). Which expression ensures that this resource is closed when the with block exits?
from contextlib import closing, nullcontext
class LegacyReader:
def __init__(self):
self.closed = False
def close(self):
self.closed = True
reader = LegacyReader()
with as active:
print("same object:", active is reader)
print("reader closed:", reader.closed)
Unlike nullcontext(), closing() really does close the object. Unlike suppress(), it does not handle or hide an exception from the block: cleanup runs and the exception still propagates. Do not use it to close a resource that belongs to the caller; ownership and cleanup responsibility must remain clear.
Use more than one context manager
A with statement can manage multiple context managers. Python processes them like nested with statements, so the later context is entered inside the earlier one and exits in the reverse order.
class Mark:
def __init__(self, name):
self.name = name
def __enter__(self):
print("enter", self.name)
def __exit__(self, exc_type, exc_value, traceback):
print("exit", self.name)
with Mark("A"), Mark("B"):
print("body")with is a cleanup tool, not just a file feature
File objects are one familiar example, but with is a general language feature. Any object that implements the context-manager protocol can define setup and cleanup behavior. The File I/O lesson teaches how to use with for files; this lesson explains the language mechanism behind it.
class Connection:
def __enter__(self):
print("connect")
return self
def send(self, message):
print("send:", message)
def __exit__(self, exc_type, exc_value, traceback):
print("disconnect")
with Connection() as connection:
connection.send("hello")Key takeaways
- with runs a block inside a context manager's runtime context.
- __enter__ runs before the block and supplies the value used by as, when present.
- __exit__ runs after a successfully entered context, including when the block raises an exception.
- A false __exit__ result lets an exception propagate; a true result suppresses it.
- Multiple context managers behave like nested with statements and exit in reverse order.
Thanks for your feedback.
Mark this learning resource complete to track your learning progress.