A context manager is an object that sets something up and guarantees it gets cleaned up — no matter what happens in between. Think of it like a hotel check-in: we arrive (__enter__), stay and do our thing, and the hotel ensures checkout happens (__exit__) even if there’s a fire alarm.
The Problem Context Managers Solve
Without context managers, we have to remember to clean up resources manually. And if an exception happens before cleanup, we’re in trouble.
# Without context manager — risky
f = open("data.txt")
content = f.read()
f.close() # what if an error happens before this line?
With a context manager, cleanup is guaranteed:
# With context manager — safe
with open("data.txt") as f:
content = f.read()
# f.close() happens automatically, even if an error occurs
How It Works: enter and exit
The with statement calls two special methods on the object:
__enter__()— runs at the start, returns something we can use (theasvariable)__exit__()— runs at the end, handles cleanup and any exceptions
with MyManager() as obj:
__enter__() called → returns obj
__exit__(exc_type, exc_val, exc_tb) called → cleanup
__exit__ returns True → exception is suppressed. Otherwise → re-raised.
Writing a Class-Based Context Manager
We just need a class with __enter__ and __exit__ methods.
class Timer:
def __enter__(self):
import time
self.start = time.time()
return self # this becomes the 'as' variable
def __exit__(self, exc_type, exc_val, exc_tb):
import time
elapsed = time.time() - self.start
print(f"Took {elapsed:.2f} seconds")
return False # don't suppress exceptions
with Timer():
total = sum(range(1_000_000))
# Prints: Took 0.03 seconds
The __exit__ method receives three arguments about any exception that occurred. If no exception, all three are None.
The @contextmanager Decorator
Writing a whole class just for setup/teardown can feel heavy. The contextlib module gives us a decorator that turns a generator function into a context manager.
from contextlib import contextmanager
@contextmanager
def timer():
import time
start = time.time()
yield # everything before yield = __enter__, after = __exit__
elapsed = time.time() - start
print(f"Took {elapsed:.2f} seconds")
with timer():
total = sum(range(1_000_000))
Everything before yield is the setup (__enter__). Everything after yield is the cleanup (__exit__). If we need to return a value, we yield it.
@contextmanager
def open_db():
conn = create_connection()
try:
yield conn # caller gets the connection
finally:
conn.close() # cleanup always happens
Common Uses
Context managers pop up everywhere in Python:
- File handling —
with open(...) as f - Database connections —
with db.connect() as conn - Locks —
with threading.Lock() - Temporary directories —
with tempfile.TemporaryDirectory() as d - Suppressing exceptions —
with contextlib.suppress(FileNotFoundError)
Nested with Statements
We can nest them or use a single with for multiple managers:
# Both are equivalent
with open("input.txt") as src, open("output.txt", "w") as dst:
dst.write(src.read())
In simple language, context managers are Python’s way of saying “I’ll handle the cleanup, no matter what.” We just focus on the work inside the with block, and Python takes care of the rest.