Python Context Managers Explained - with Statement, __enter__, __exit__ & contextlib | Tutorial #25
Video: Python Context Managers Explained - with Statement, __enter__, __exit__ & contextlib | Tutorial #25 by Taught by Celeste AI - AI Coding Coach
Python Context Managers: with, enter, exit, @contextmanager
withstatement guarantees setup/teardown. Implement via__enter__/__exit__on a class, or@contextmanageron a generator function. Used for files, locks, DB connections, timers — anywhere you need "do this, then make sure cleanup happens."
You've used with open(...) since lesson 15. Now we'll see how it works and how to write your own.
The with statement
with open("file.txt", "r") as f:
content = f.read()
# f is closed here, even if read() raised
with calls open(), binds the result to f, runs the block, and then calls cleanup — even if an exception is raised.
The pattern: setup → use → teardown, with teardown guaranteed.
enter and exit
A context manager is any class that implements two methods:
import time
class Timer:
def __enter__(self):
self.start = time.time()
print("Timer started")
return self # value bound to `as` variable
def __exit__(self, exc_type, exc_val, exc_tb):
elapsed = time.time() - self.start
print(f"Elapsed: {elapsed:.4f}s")
return False # False means: don't swallow exceptions
with Timer() as t:
total = sum(range(1_000_000))
__enter__runs at the start of thewithblock. Whatever it returns is bound to theasvariable.__exit__runs at the end (always). Receives the exception info if one was raised, otherwise threeNones.
The with Timer() as t: is roughly:
mgr = Timer()
t = mgr.__enter__()
try:
... # block body
except:
if not mgr.__exit__(*sys.exc_info()):
raise
else:
mgr.__exit__(None, None, None)
Suppressing exceptions
__exit__ can return True to suppress an exception:
class SafeDiv:
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
if exc_type is ZeroDivisionError:
print(f"Caught: {exc_val}")
return True # suppress
return False # propagate
with SafeDiv():
result = 10 / 0
print("never runs")
print("program continues")
Use sparingly — silently swallowing exceptions hides bugs. The right time is when you've genuinely handled the failure (logged, defaulted, etc.) and want to continue.
@contextmanager: generator-based
Writing a class is verbose. contextlib.contextmanager lets you write context managers as generators:
from contextlib import contextmanager
import time
@contextmanager
def timer(label):
start = time.time()
print(f"{label}: started")
try:
yield start
finally:
elapsed = time.time() - start
print(f"{label}: {elapsed:.4f}s")
with timer("Loop") as t:
total = sum(range(500_000))
The decorator turns the generator into a context manager:
- Code before
yieldis__enter__. - The yielded value is what
asgets bound to. - Code after
yieldis__exit__.
Wrap in try/finally to guarantee cleanup even if the body raises.
Change directory: a worked example
import os
from contextlib import contextmanager
@contextmanager
def change_dir(path):
old = os.getcwd()
os.chdir(path)
try:
yield
finally:
os.chdir(old)
with change_dir("/tmp"):
print(os.getcwd()) # /tmp
# do stuff in /tmp
print(os.getcwd()) # back to original
This is the use case for context managers: temporarily change global state, ensure restoration.
Built-in context managers
A few you'll see:
# File I/O
with open("file.txt") as f:
...
# Threading lock
import threading
lock = threading.Lock()
with lock:
# only one thread here at a time
# Suppressing specific exceptions
from contextlib import suppress
with suppress(FileNotFoundError):
os.remove("maybe_exists.txt")
# Redirecting stdout
from contextlib import redirect_stdout
with open("log.txt", "w") as f:
with redirect_stdout(f):
print("This goes to log.txt")
# Decimal precision
from decimal import localcontext
with localcontext() as ctx:
ctx.prec = 50
# extra precision in this block
ExitStack: composing context managers
from contextlib import ExitStack
with ExitStack() as stack:
files = [stack.enter_context(open(name)) for name in filenames]
# all files closed when block exits
ExitStack lets you manage a dynamic number of context managers. Useful when you don't know how many at compile time.
Multiple context managers
with open("input.txt") as fin, open("output.txt", "w") as fout:
fout.write(fin.read())
Comma-separated. Each manager's __exit__ runs in reverse order.
For Python 3.10+, parenthesized form for readability across lines:
with (
open("input.txt") as fin,
open("output.txt", "w") as fout,
Lock() as lock,
):
...
Error handling inside context managers
@contextmanager
def database():
conn = connect()
try:
yield conn
conn.commit() # commit on success
except Exception:
conn.rollback() # rollback on failure
raise # re-raise
finally:
conn.close()
Idiomatic transaction management. Commit if the block succeeded, rollback if it raised, always close. The pattern shows up everywhere: HTTP sessions, file locks, anything with paired open/close.
Re-entrance and reuse
mgr = Timer()
with mgr: # works
...
with mgr: # works again only if Timer is reentrant
...
Class-based managers can be reused if __enter__ is idempotent. Generator-based ones (@contextmanager) are single-use — calling with mgr: after consumption raises RuntimeError. Create a fresh manager each time.
When NOT to use a context manager
- One-off cleanup that's clear from context (a
try/finallyis sometimes simpler). - "Setup, do thing, teardown" but the setup/teardown isn't paired (just call functions).
The rule: context managers shine when the setup/teardown coupling is invariant — same teardown for every use.
Common stumbles
Forgetting to yield in @contextmanager. The function never enters the with block; instead it raises RuntimeError.
Yielding the wrong thing. yield provides the as value. If you yield nothing, as gets None.
Bare try without finally. Cleanup must be in finally, not just after yield. Otherwise an exception in the block skips cleanup.
return in __exit__ confusion. return True suppresses, return False (or no return) propagates. Default behavior should be propagation.
Reusing a generator-based manager. Single use. Make a fresh mgr = my_manager() each time, or convert to a class.
Catching BaseException. __exit__ shouldn't swallow KeyboardInterrupt or SystemExit. Be specific about what you handle.
What's next
Lesson 26: regular expressions. re.match, re.search, re.findall, re.sub, character classes, groups.
Recap
with mgr as x: ensures setup/teardown. Class form: __enter__ (returns as value) and __exit__(exc_type, exc_val, exc_tb) (return True to suppress). Generator form: @contextmanager decorator on a function with try/yield/finally. Use for file I/O, locks, DB transactions, temporarily modifying global state. ExitStack for dynamic composition. Single-use vs reentrant — know the difference.
Next lesson: regular expressions.