Python Context Managers
A context manager is an object that knows how to set something up and reliably tear it down, even if an error happens in between. The most common way you use one is with the with statement — for example, opening a file and having Python close it automatically once you’re done. Context managers are the reason you rarely see close() calls scattered through well-written Python code, and understanding how they work lets you write safer code for files, locks, database connections, and any resource that needs cleanup.
Overview / How it works
Whenever you write with open("data.txt") as f:, Python isn’t doing anything magical — it’s calling two special methods on the object returned by open(). Any object that defines __enter__ and __exit__ follows the context manager protocol, and can be used after with.
Here’s what actually happens, in order:
- Python evaluates the expression after
with(e.g.open("data.txt")) to get a context manager object. - Python calls that object’s
__enter__()method. Whatever__enter__()returns is bound to the variable afteras(if there is one). - The indented block runs.
- When the block finishes — whether normally, via
return, or because an exception was raised — Python calls the object’s__exit__(exc_type, exc_value, traceback)method. - If no exception occurred,
__exit__is called with threeNonearguments. If an exception occurred,__exit__receives the exception’s type, value, and traceback object. - If
__exit__returns a truthy value, Python suppresses the exception (pretends it never happened). If it returnsNoneor any falsy value, the exception propagates normally after cleanup runs.
This is exactly equivalent to writing a try/finally block by hand, except the cleanup logic lives inside the object itself instead of being repeated at every call site. That’s the whole point: it moves “remember to clean up” out of your control flow and into the type system, so you can’t forget it.
Under the hood, a file object’s __enter__ simply returns self (which is why as f gives you the file object), and its __exit__ calls self.close() and returns False so exceptions still propagate. This is also why nesting is easy — Python doesn’t have any special-case logic for with beyond calling these two methods, so any object can plug into it.
Syntax
with expression as variable:
# block that uses variable
...
# cleanup already happened here, even if an exception was raised
with expr1 as var1, expr2 as var2:
# multiple context managers, entered left to right,
# exited right to left when the block ends
...
expression— any expression that evaluates to an object implementing__enter__and__exit__.as variable— optional; binds the value returned by__enter__(). Many context managers (like locks) don’t return anything useful and omit it.- Multiple managers separated by commas are equivalent to nesting
withstatements; if the second one’s__enter__raises, the first one’s__exit__still runs.
Examples
Example 1: The classic file-handling case
with open("notes.txt", "w") as f:
f.write("first line\n")
f.write("second line\n")
print(f.closed)
Output:
True
Even though the file object f still exists after the block (variables from a with block aren’t deleted), its __exit__ method already called close() on it, so f.closed is True. If you tried to write to f after the block, you’d get a ValueError because the underlying file handle is gone.
Example 2: Writing your own context manager with a class
import time
class Timer:
def __enter__(self):
self.start = time.perf_counter()
return self
def __exit__(self, exc_type, exc_value, traceback):
self.elapsed = time.perf_counter() - self.start
print(f"Elapsed: {self.elapsed:.4f} seconds")
return False
with Timer() as t:
total = sum(range(1_000_000))
print(f"Total was {total}")
Output:
Elapsed: 0.0XXX seconds
Total was 499999500000
The exact elapsed time varies each run, but the structure is fixed: __enter__ records the start time and returns self so t refers to the Timer instance, and __exit__ always runs afterward — printing the elapsed time whether or not the block raised an error. Returning False from __exit__ means any exception inside the block would still propagate after this cleanup.
Example 3: A context manager that suppresses exceptions
class IgnoreZeroDivision:
def __enter__(self):
return None
def __exit__(self, exc_type, exc_value, traceback):
if exc_type is ZeroDivisionError:
print("Caught a division by zero, ignoring it.")
return True
return False
with IgnoreZeroDivision():
print("Before the risky operation")
result = 10 / 0
print("This line never runs")
print("Program continues normally")
Output:
Before the risky operation
Caught a division by zero, ignoring it.
Program continues normally
Because __exit__ returns True when it sees a ZeroDivisionError, Python swallows that exception instead of letting it propagate. Notice that the line after the division (print("This line never runs")) is skipped — the exception still aborts the rest of the with block itself; only propagation past the block is suppressed. This is the same mechanism contextlib.suppress uses internally.
Example 4: The easier way — contextlib.contextmanager
from contextlib import contextmanager
@contextmanager
def managed_resource(name: str):
print(f"Acquiring {name}")
try:
yield name.upper()
finally:
print(f"Releasing {name}")
with managed_resource("database") as handle:
print(f"Using {handle}")
Output:
Acquiring database
Using DATABASE
Releasing database
The @contextmanager decorator turns a generator function into a context manager without writing a class at all. Code before the yield becomes __enter__, the yielded value becomes what as handle binds to, and code after the yield (typically in a finally) becomes __exit__. If an exception happens inside the with block, it’s raised at the point of the yield inside the generator, so wrapping the yield in try/finally guarantees the release code still runs.
How it works step by step / Under the hood
- Python looks up
__enter__and__exit__on the object’s type, not the instance — this is a general rule for special methods, so defining them only viainstance.__enter__ = ...won’t work. - The value bound by
asis whatever__enter__()returns — it does not have to be the context manager itself. In Example 4,as handlegets the yielded string, not the generator object. __exit__always runs before the exception (if any) continues propagating up the call stack, so any code after thewithblock only executes if the exception was suppressed or there was none.- With multiple comma-separated managers, exit order is the reverse of enter order — just like nested
withblocks — which matters when managers depend on each other (e.g. a lock that must be released after the connection using it is closed).
Common Mistakes
Mistake 1: Forgetting that __exit__ must return True to suppress an exception
class LoggingContext:
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, traceback):
if exc_type is not None:
print(f"An error occurred: {exc_value}")
# forgot to return True or False explicitly
Since the function has no explicit return, it returns None, which is falsy — so the exception still propagates after the message is printed. That’s often what you want, but if the intent was to suppress it, this is a bug. Always return an explicit True or False from __exit__ so the behavior is unambiguous, rather than relying on the implicit None.
Mistake 2: Using contextmanager without try/finally around the yield
from contextlib import contextmanager
@contextmanager
def bad_resource():
print("opening")
yield
print("closing") # only runs if no exception was raised!
If the code inside the with block raises, the generator’s yield re-raises that exception at the yield point, and execution never reaches the print("closing") line — the resource leaks. The fix is to always wrap the cleanup in try/finally:
from contextlib import contextmanager
@contextmanager
def good_resource():
print("opening")
try:
yield
finally:
print("closing") # always runs
Mistake 3: Assuming a with block creates a new scope
with open("config.txt") as f:
data = f.read()
print(data) # fine, data still accessible
print(f.closed) # also accessible, but f is now unusable for reading
Unlike some languages, Python’s with doesn’t create a new variable scope — f and data remain defined after the block. The mistake is assuming f is unusable or gone; it’s still there, just closed, so calling f.read() afterward raises a ValueError.
Best Practices
- Prefer
withover manualtry/finallywhenever a library provides a context manager (files, locks, database cursors,tempfileobjects) — it’s shorter and harder to get wrong. - For simple setup/teardown logic, reach for
@contextlib.contextmanagerfirst; write a full class only when you need to hold state across multiple methods or support re-entry. - Always wrap the
yieldin a generator-based context manager withtry/finallyso cleanup runs even when the block raises. - Have
__exit__returnFalseexplicitly (or nothing, understanding that’s falsy) unless you specifically intend to swallow exceptions — silently eating errors makes bugs hard to trace. - Use
contextlib.suppress(SomeError)instead of writing a custom class when all you want is to ignore a specific exception type. - Combine multiple related resources in one
withstatement (with open(a) as fa, open(b) as fb:) instead of nesting indentation levels, for readability. - Never rely on
__del__as a substitute for a context manager — its timing is not guaranteed, especially with reference cycles or alternate Python implementations.
Practice Exercises
- Write a class-based context manager
Suppress404that, when used in awithblock, catches a customNotFoundErrorexception and prints"Resource not found, skipping."instead of letting it propagate. Any other exception type should propagate normally. - Using
@contextlib.contextmanager, write atemporary_value(obj, attr, new_value)context manager that temporarily setsobj.attrtonew_valuefor the duration of thewithblock, and restores the original value afterward — even if the block raises an exception. - Predict the output of a program that opens two files with a single comma-separated
with open(a) as fa, open(b) as fb:statement, where opening the second file raisesFileNotFoundError. Which file’s__exit__runs, and which doesn’t? Then verify your reasoning by writing and reading through the code.
Summary
- A context manager is any object implementing
__enter__and__exit__; thewithstatement calls these automatically. __enter__()‘s return value is bound byas variable;__exit__receives exception info (orNones) and runs no matter how the block exits.- Returning a truthy value from
__exit__suppresses the exception; returning falsy (including nothing) lets it propagate after cleanup. contextlib.contextmanagerlets you write a context manager as a generator function with a singleyield, avoiding a full class — just remembertry/finallyaround theyield.- Multiple context managers can share one
withstatement, entering left-to-right and exiting right-to-left. - Context managers guarantee cleanup runs, which is why they’re the standard tool for files, locks, network connections, and any other resource that must be released deterministically.
