The earlier File Handling lesson used with open('file.txt') as f: without explaining why. A context manager guarantees a cleanup step runs — closing a file, releasing a lock, closing a database connection — even if an error happens in between.
with open('notes.txt') as f:
content = f.read()
# f.close() runs automatically here — even if the code above raised an errorWithout with, the same safety needs a manual try/finally — the whole point of a context manager is not needing to write that by hand every time.
# The manual equivalent, for comparison
f = open('notes.txt')
try:
content = f.read()
finally:
f.close()Any class implementing these two methods can be used with with.
class Timer:
def __enter__(self):
import time
self.start = time.time()
return self
def __exit__(self, exc_type, exc_value, traceback):
import time
print(f'Elapsed: {time.time() - self.start:.2f}s')
with Timer():
slow_operation()
# The elapsed time prints automatically when the block endsFor a simple case, a generator function decorated with @contextmanager avoids writing a whole class.
from contextlib import contextmanager
import time
@contextmanager
def timer():
start = time.time()
yield
print(f'Elapsed: {time.time() - start:.2f}s')
with timer():
slow_operation()Everything before yield runs as setup (like __enter__); everything after runs as cleanup (like __exit__), and always runs even if the block raises.
| Common context manager | What it cleans up |
|---|---|
| open() | Closes the file |
| threading.Lock() | Releases the lock |
| a database connection object | Closes the connection |
| unittest.mock.patch() | Restores the original, un-mocked value |
What those three __exit__ arguments are for
__exit__ receiving exception details (exc_type, exc_value, traceback) lets a context manager react to — or even suppress — an error, not just clean up after a successful block. Most simple context managers ignore these and just do cleanup regardless.