Skip to content

Context Managers

Many operations must be paired with a cleanup step: open a file then close it, acquire a lock then release it, start a timer then stop it. If an exception occurs between the two steps, the cleanup never runs — leading to resource leaks.

The with statement solves this by guaranteeing that the cleanup runs no matter what.

with open("data.txt") as f:
content = f.read()
# f is always closed here, even if read() raised

Any object with __enter__ and __exit__ methods is a context manager.

with expr as var:
body

This is exactly equivalent to:

cm = expr
var = cm.__enter__()
try:
body
except:
if not cm.__exit__(*sys.exc_info()):
raise
else:
cm.__exit__(None, None, None)

__enter__ sets up the resource and returns a value bound to var. __exit__ always runs on exit. If it returns a truthy value it suppresses the exception; returning False (or None) lets the exception propagate.

class Timer:
def __enter__(self) -> "Timer":
print("Timer started")
return self
def __exit__(self, exc_type, exc_val, exc_tb) -> bool:
print("Timer stopped")
return False # do not suppress exceptions

The three arguments to __exit__ are the exception type, value, and traceback — all None when no exception occurred.

contextlib.contextmanager — the generator shortcut

Section titled “contextlib.contextmanager — the generator shortcut”

Writing a full class just to wrap setup/teardown is verbose. contextlib.contextmanager lets you use a generator function instead:

import contextlib
@contextlib.contextmanager
def managed_resource(name: str):
print(f"Acquiring {name}")
try:
yield name # value bound to the as-variable
finally:
print(f"Releasing {name}")

Everything before yield is __enter__ logic. Everything after yield (in the finally) is __exit__ logic. The try/finally inside the generator ensures the teardown runs even when an exception is raised inside the with block.

Multiple context managers can be combined on one with line — they nest left to right:

with open("in.txt") as src, open("out.txt", "w") as dst:
dst.write(src.read())
import contextlib
# --- class-based context manager ---
class Timer:
def __enter__(self):
print("Timer started")
return self
def __exit__(self, exc_type, exc_val, exc_tb):
print("Timer stopped")
return False
print("=== class-based ===")
with Timer():
print(" doing work...")
# --- contextlib.contextmanager ---
@contextlib.contextmanager
def managed_resource(name: str):
print(f"Acquiring {name}")
try:
yield name
finally:
print(f"Releasing {name}")
print("\n=== contextmanager generator ===")
with managed_resource("connection") as res:
print(f" using {res}")
# --- cleanup on error ---
print("\n=== cleanup runs even on error ===")
try:
with managed_resource("db") as res:
print(f" using {res}")
raise RuntimeError("something failed")
except RuntimeError as e:
print(f" caught: {e}")
What two methods must an object implement to be used as a context manager?
In a `@contextlib.contextmanager` generator, what marks the boundary between setup and teardown?
When does the `__exit__` method run?
What happens if `__exit__` returns a truthy value when an exception occurred?