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
Trace the with Timer(): block event by event:
Timer()creates the object, like any other class.withcalls its__enter__. The start time is recorded. Whatever__enter__returns is what anasclause receives; returningselfis the usual choice so the block can use the object.- Your indented block runs.
- The block ends, either normally or by raising an error, and
withcalls__exit__either way. The elapsed time is computed and printed.
The three arguments to __exit__ describe what went wrong, if anything: the exception's class, the exception object, and its traceback. In the normal no-error case, all three are None. You usually ignore them and return nothing, which lets any error continue outward after your cleanup has run. Returning True instead tells Python "I handled it, swallow the exception", which is rarely what you 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.
If the yield looks familiar, it should: timer is a generator, using the same pause-and-resume mechanism from the generators lesson. with runs the body up to yield (that is the setup), pauses it while your indented block runs, then resumes it afterward so the finally cleanup executes. Same trick, put to a new use.
| 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.

