The with Statement
Why Context Managers Matters
The Problem: Cleanup code in `finally` blocks gets repeated, forgotten, and broken by early returns.
The Solution: Context managers (`with`) guarantee setup/teardown around a block, encapsulate resource lifecycle, and compose cleanly. contextlib turns one-shot helpers into reusable managers in two lines.
Real Impact: Every file handle, DB connection, lock, transaction, and timer in production Python should be inside a `with` — it's the simplest tool for correctness.
Real-World Analogy
Think of a context manager as an automatic door:
- __enter__ = the door opening when you arrive — sets up the resource
- Body of with = you walk through and do your work
- __exit__ = the door closing behind you — always, even if you tripped
- contextlib.suppress = ignoring 'wet floor' signs on the way out
- ExitStack = a row of doors that close in reverse order
A context manager guarantees cleanup happens — even on exceptions. The canonical example is open().
# Without with — fragile
f = open("data.txt")
try:
data = f.read()
finally:
f.close()
# With — concise and exception-safe
with open("data.txt") as f:
data = f.read()
# f is guaranteed closed here, even if read() raises
Multiple Contexts
with open("in.txt") as src, open("out.txt", "w") as dst:
dst.write(src.read())
Writing Your Own Context Manager
Class-based: __enter__ and __exit__
import time
class Timer:
def __init__(self, label):
self.label = label
def __enter__(self):
self.start = time.perf_counter()
return self
def __exit__(self, exc_type, exc, tb):
elapsed = time.perf_counter() - self.start
print(f"{self.label}: {elapsed*1000:.2f}ms")
# Return True to swallow the exception (don't, normally)
return False
with Timer("db query"):
run_query()
Function-based: @contextmanager
from contextlib import contextmanager
@contextmanager
def timer(label):
start = time.perf_counter()
try:
yield # body of `with` runs here
finally:
elapsed = time.perf_counter() - start
print(f"{label}: {elapsed*1000:.2f}ms")
with timer("db query"):
run_query()
Prefer @contextmanager for simple cases
For one-off setup/teardown, the decorator form is half the code. Use class-based when you need to expose other methods or hold complex state.
contextlib Helpers
contextlib.closing
from contextlib import closing
from urllib.request import urlopen
with closing(urlopen(url)) as resp:
data = resp.read()
# closing() makes any object with .close() into a context manager
contextlib.suppress
from contextlib import suppress
with suppress(FileNotFoundError):
os.remove("tmp.txt")
# Equivalent to try/except FileNotFoundError: pass
contextlib.ExitStack
Dynamic numbers of context managers — open N files known at runtime.
from contextlib import ExitStack
with ExitStack() as stack:
files = [stack.enter_context(open(p)) for p in paths]
for f in files:
process(f.read())
# All N files closed in reverse order
chdir (3.11+)
from contextlib import chdir
with chdir("/tmp"):
do_work()
# cwd is restored on exit
Async Context Managers
from contextlib import asynccontextmanager
import aiohttp
@asynccontextmanager
async def api_session(token):
session = aiohttp.ClientSession(headers={"Authorization": f"Bearer {token}"})
try:
yield session
finally:
await session.close()
async def main():
async with api_session(token) as s:
async with s.get("/users") as r:
print(await r.json())
Real-World Patterns
Database transactions
@contextmanager
def transaction(conn):
try:
yield conn
conn.commit()
except:
conn.rollback()
raise
with transaction(conn) as tx:
tx.execute("INSERT ...")
tx.execute("UPDATE ...")
# commit on success, rollback on exception
Temporary monkey-patching for tests
@contextmanager
def override_setting(name, value):
original = settings[name]
settings[name] = value
try:
yield
finally:
settings[name] = original
with override_setting("DEBUG", True):
run_test()
🎯 Practice Exercises
Exercise 1: Temporary directory
Write a tmp_dir() context manager that creates a tempdir, yields its path, and removes it on exit. Compare with tempfile.TemporaryDirectory.
Exercise 2: Suppress on demand
Build a configurable suppressor: with maybe_suppress(KeyError, when=is_test):.
Exercise 3: Multi-file pipeline
Use ExitStack to open N files (count from argv) and write their concatenation to stdout.
Exercise 4: Async timer
Build an @asynccontextmanager that times an async block.