Context managers (with)
A context manager is the machinery behind with open(...) as f:. The with block guarantees that some setup runs at the start and some teardown runs at the end — even if an exception is raised in the middle. That's the whole point: cleanup you can't forget and can't skip.
The protocol is two methods. __enter__ runs when you enter the block and its return value is bound to the as name. __exit__ runs when you leave — normally OR because of an error. Python calls __exit__(exc_type, exc_val, exc_tb) with the exception details (or three Nones if all went well). If __exit__ returns a truthy value it *swallows* the exception; return False (or nothing) to let it propagate.
Writing a full class is overkill for simple cases, so contextlib.contextmanager lets you write one as a generator: everything before the single yield is the setup, the yield hands control to the with body, and everything after is the teardown. Wrap the yield in try/finally if the cleanup must run on errors too.
Use context managers for anything that's acquired-then-released: files, database connections, locks, temporary directories, timers, or any "do X, then guarantee un-X".
Syntax
# Class-based:
class Managed:
def __enter__(self):
# setup; return value goes to `as`
return self
def __exit__(self, exc_type, exc_val, exc_tb):
# teardown — always runs
return False # False = don't suppress exceptions
with Managed() as m: ...
# Generator-based (contextlib):
import contextlib
@contextlib.contextmanager
def managed():
# setup
yield resource
# teardownWorked examples
Cleanup runs even when the block raises
log = []
class Resource:
def __enter__(self):
log.append("open")
return self
def __exit__(self, exc_type, exc_val, exc_tb):
log.append("close") # runs no matter what
return False # let the error propagate
try:
with Resource():
log.append("work")
raise RuntimeError("boom")
except RuntimeError:
log.append("caught")
print(log)
# Output:
# ['open', 'work', 'close', 'caught']A generator-based timer with @contextlib.contextmanager
import contextlib
@contextlib.contextmanager
def timer(label):
print(f"start {label}") # setup (before yield)
yield # the with-body runs here
print(f"end {label}") # teardown (after yield)
with timer("job"):
print("running")
# Output:
# start job
# running
# end jobSuppressing chosen exceptions, propagating the rest
import contextlib
@contextlib.contextmanager
def suppress(*types):
try:
yield
except Exception as e:
if not isinstance(e, types):
raise # not in the list — re-raise
with suppress(ValueError):
raise ValueError("silenced")
print("survived")
# Output:
# survivedCommon mistakes & gotchas
__exit__ must accept three exception arguments
Even when nothing goes wrong, Python calls __exit__(self, exc_type, exc_val, exc_tb) with three values (all None on success). Defining def __exit__(self): raises TypeError. Use def __exit__(self, exc_type, exc_val, exc_tb): or def __exit__(self, *exc):.
Returning True from __exit__ silently eats errors
If __exit__ returns a truthy value, the exception inside the with block is suppressed and your program carries on as if nothing happened — a great way to hide bugs. Return False (or nothing, which is None/falsy) unless you genuinely mean to swallow the error.
Generator context managers need try/finally for guaranteed cleanup
In an @contextmanager, if the with body raises, the exception is thrown *at* the yield — so code plainly written after yield is skipped. If the teardown must always run (closing a file, releasing a lock), wrap it: try: yield / finally: cleanup().
Why it matters
Resource leaks — unclosed files, dangling connections, locks never released — are some of the nastiest production bugs because they show up only under load. Context managers make correct cleanup the default and the readable choice, which is why `with` is everywhere in mature Python code.