Context Managers and the with Statement
Every time you open a file, connect to a database, or acquire a lock, you also need to clean up afterward, even if an error happens halfway through. Context managers and the with statement guarantee that cleanup runs. You already use with open(...); this lesson shows you why it matters and how to write your own.
What You'll Learn
- Why
withis safer than manual open/close - How
__enter__and__exit__work - The easy way to write a context manager with
contextlib - Real uses: files, timers, and temporary settings
The Problem: Forgetting to Clean Up
Without with, you have to remember to close resources, and an exception can skip your cleanup entirely:
f = open("data.csv")
data = f.read() # if this raises, the next line never runs
f.close() # file stays open, leaking a resource
The with statement closes the file automatically, even if an error is raised inside the block:
with open("data.csv") as f:
data = f.read()
# file is guaranteed closed here, error or not
That guarantee is the entire point. Open connections, file handles, and locks that never get released cause some of the nastiest bugs in long-running data jobs.
How It Works: enter and exit
Any object with __enter__ and __exit__ methods is a context manager. with calls __enter__ at the top of the block and __exit__ when the block ends, no matter how it ends.
class Timer:
def __enter__(self):
import time
self.start = time.perf_counter()
return self # value bound to "as"
def __exit__(self, exc_type, exc_value, traceback):
import time
self.elapsed = time.perf_counter() - self.start
print(f"block took {self.elapsed:.3f}s")
# returning False (or None) lets any exception propagate
with Timer():
total = sum(x * x for x in range(1_000_000))
# block took 0.05s
__exit__ receives details about any exception that occurred. Return False (or nothing) to let the exception propagate normally; that is what you usually want.
The Easy Way: @contextmanager
Writing two methods is overkill for simple cases. The contextlib module lets you write a context manager as a single generator function. Everything before yield is setup; everything after is cleanup.
from contextlib import contextmanager
import time
@contextmanager
def timer(label):
start = time.perf_counter()
try:
yield # the with-block runs here
finally:
elapsed = time.perf_counter() - start
print(f"{label}: {elapsed:.3f}s")
with timer("training"):
total = sum(range(1_000_000))
The try/finally ensures the cleanup runs even if the block raises. This generator style is the most common way to write context managers in real code.
| Criteria | Class style | @contextmanager style |
|---|---|---|
| You write | __enter__ and __exit__ | One generator with yield |
| Best for | Reusable objects, complex state | Quick setup/teardown wrappers |
| Cleanup goes in | __exit__ | the finally block |
Class style
- You write
- __enter__ and __exit__
- Best for
- Reusable objects, complex state
- Cleanup goes in
- __exit__
@contextmanager style
- You write
- One generator with yield
- Best for
- Quick setup/teardown wrappers
- Cleanup goes in
- the finally block
Real Uses Beyond Files
- Database connections and sessions that must be committed or rolled back and then closed.
- Timers around training or processing steps, as shown above.
- Temporary settings: switch a config value, run a block, then restore it in cleanup.
- Locks in concurrent code, acquired on enter and released on exit.
Try It
Run this. The context manager prints on enter and exit. Add a line inside the with block that raises an error (for example raise ValueError("boom")) and confirm the cleanup still runs.
Key Takeaways
- The
withstatement guarantees cleanup runs even if the block raises an error. - A context manager implements
__enter__(setup) and__exit__(cleanup). - The value after
asis whatever__enter__returns. @contextmanagerlets you write one as a generator: setup,yield, then cleanup in afinally.- Use it for files, database sessions, timers, locks, and temporary settings.

