A context manager is an object that defines __enter__ and __exit__ methods. When used in a with statement, Python calls __enter__ at the start (setup) and __exit__ at the end (teardown, even if an exception was raised). The with statement is defined in the Python Language Reference as the primary mechanism for resource management.
Guaranteed setup and teardown with with
What you'll learn: What the with statement does, why it guarantees cleanup even on exceptions, and which built-in objects support it. After this, you'll never open a file without with again.
How to read this tab: Deliberately cause an exception inside a with open() block and observe that the file is still properly closed. Then try the same without with and see the file handle left open.
A context manager is like a responsible adult who not only opens a door for you but guarantees they'll close it when you're done — even if you trip and fall while going through. The with statement is the door; __exit__ is the closing.
with is not just a style preference — it's the only safe way to manage resources. try/finally is equivalent but verbose. The with statement makes the contract explicit: this block uses this resource, and the resource is always cleaned up when the block exits. Files, locks, database connections, network sockets — all should be managed with with.
The with statement
The most common context manager in Python is file handling. Without with, you must remember to call f.close() — and if an exception occurs before that line, the file stays open. With with, the file is always closed, regardless of what happens.
# WITHOUT with — dangerous: close() may not run on exception
f = open("data.txt", "r")
try:
content = f.read()
finally:
f.close() # must remember this
# WITH with — always closes, even on exception
with open("data.txt", encoding="utf-8") as f:
content = f.read()
# f.close() is called automatically here — always
# Multiple context managers in one with (Python 3.1+)
with open("input.txt") as src, open("output.txt", "w") as dst:
dst.write(src.read())
# Parenthesised form for many managers (Python 3.10+)
with (
open("a.txt") as a,
open("b.txt") as b,
open("c.txt") as c,
):
data = a.read() + b.read() + c.read()The most important context manager you're not using: unittest.mock.patch(). Use it as a context manager (not just a decorator) when you need mock to be active for only part of a test: with patch("module.func") as mock_func:. The mock is automatically removed when the with block exits, even on test failure.
Common built-in context managers
Many Python built-ins and standard library modules support the context manager protocol:
| Context Manager | Manages | On exit |
|---|---|---|
open() | File handles | Closes the file |
threading.Lock() | Thread locks | Releases the lock |
decimal.localcontext() | Decimal precision | Restores old precision |
unittest.mock.patch() | Mocked objects | Restores original |
contextlib.suppress() | Expected exceptions | Silences named exceptions |
| Database connections | DB connections | Commits or rolls back |
✅ Beginner tab complete
- I always use "with open(...) as f:" — never open() without with
- I know with guarantees __exit__ runs even if an exception occurs inside the block
- I can list 4 common Python objects that support the context manager protocol
- I can use multiple context managers in a single with statement
Implementing __enter__ and __exit__, and @contextmanager
What you'll learn: How to implement the full context manager protocol (__enter__ and __exit__). How to write context managers as generator functions using @contextlib.contextmanager. contextlib.suppress for silently ignoring specific exceptions.
How to read this tab: Write a Timer context manager class from scratch (enter records start time, exit prints elapsed). Then rewrite it as a @contextmanager generator. Compare the two approaches — both work; choose based on complexity.
Returning True from __exit__ suppresses the exception — use with care. If your __exit__ always returns True, it silently swallows ALL exceptions. This hides bugs. Only return True when you specifically intend to suppress the exception (like contextlib.suppress does for named exception types). For most context managers, return False or None — let exceptions propagate.
The __enter__ and __exit__ protocol
Any object implementing __enter__ and __exit__ can be used in a with statement. __enter__ is called at the start and its return value is bound to the as variable. __exit__ is called at the end, receiving exception information if one occurred — it can suppress the exception by returning a truthy value.
class Timer:
"""Context manager that measures elapsed time."""
import time
def __enter__(self):
import time
self.start = time.perf_counter()
return self # bound to the 'as' variable
def __exit__(self, exc_type, exc_val, exc_tb):
import time
self.elapsed = time.perf_counter() - self.start
print(f"Elapsed: {self.elapsed:.4f}s")
return False # False = don't suppress exceptions
with Timer() as t:
total = sum(range(10_000_000))
# Elapsed: 0.312s
print(t.elapsed) # also accessible after the with block
# __exit__ signature: exc_type, exc_val, exc_tb
# If no exception: all three are None
# If exception: they contain the exception info
# Return True to suppress the exception; False/None to propagate it
class SuppressIOError:
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
return exc_type is IOError # suppress IOErrors only@contextmanager is the shortest path to a working context manager. Code before yield = __enter__. yield value = the value bound to the as variable. Code in finally after yield = __exit__ cleanup. Wrap the yield in try/finally to ensure cleanup always runs. This is the pattern used by the Python standard library itself for most of its context managers.
Writing context managers with contextlib
The contextlib module's @contextmanager decorator lets you write a context manager as a generator function — simpler than implementing the full class protocol. Everything before yield is the __enter__ body; everything after is the __exit__ body.
from contextlib import contextmanager
import os, tempfile
@contextmanager
def temporary_directory():
"""Create a temp dir, yield it, delete it on exit."""
tmpdir = tempfile.mkdtemp()
try:
yield tmpdir # __enter__ returns this value
finally:
import shutil
shutil.rmtree(tmpdir) # __exit__ cleanup
with temporary_directory() as tmpdir:
# work inside the temp directory
filepath = os.path.join(tmpdir, "data.txt")
with open(filepath, "w") as f:
f.write("temporary data")
# tmpdir is deleted here, regardless of exceptions
# contextlib.suppress — the cleanest way to ignore specific exceptions
from contextlib import suppress
with suppress(FileNotFoundError):
os.remove("might_not_exist.txt")
# No try/except needed — FileNotFoundError is silently ignoredopen(), __enter__ returns the file object, not the open() return value. For threading.Lock(), __enter__ returns the lock. For your own context managers, return whatever the user needs to work with inside the block.✅ Intermediate tab complete
- I can implement __enter__ and __exit__ on a class to make it a context manager
- I know __exit__ receives (exc_type, exc_val, exc_tb) — all None if no exception
- I can write a context manager using @contextmanager with yield as the boundary
- I know contextlib.suppress(FileNotFoundError) as an alternative to try/except/pass
ExitStack, asynccontextmanager, and the with desugar
What you'll learn: contextlib.ExitStack for managing a dynamic number of context managers. asynccontextmanager and async with for async context management. The precise desugar of the with statement per the Python Language Reference.
How to read this tab: Build a function that opens N files from a list (unknown at write time) using ExitStack. This pattern appears in test fixtures, batch processors, and connection pools.
contextlib advanced tools
The contextlib module provides several advanced context manager utilities. ExitStack manages a dynamic stack of context managers — useful when the number of managers is not known until runtime. nullcontext is a no-op context manager useful for optional context management. AsyncExitStack and asynccontextmanager provide async equivalents.
from contextlib import ExitStack, nullcontext
# ExitStack — dynamic number of context managers
def process_files(filenames):
with ExitStack() as stack:
files = [stack.enter_context(open(f)) for f in filenames]
# All files opened; all will be closed on exit regardless of exceptions
return [f.read() for f in files]
# nullcontext — conditional context management
def process(data, output_file=None):
with (open(output_file, "w") if output_file else nullcontext()) as f:
result = compute(data)
if f:
f.write(result)
return result
# asynccontextmanager for async with
from contextlib import asynccontextmanager
@asynccontextmanager
async def async_timer():
import time
start = time.perf_counter()
try:
yield
finally:
print(f"Async elapsed: {time.perf_counter() - start:.4f}s")
async def main():
async with async_timer():
import asyncio
await asyncio.sleep(0.1)The Python Language Reference specifies that the with statement desugars to a precise sequence of operations: evaluate the context expression, call __enter__, bind the result to the as target, execute the body, and unconditionally call __exit__ with exception information (or all-None if no exception). This precise specification means the with statement's behaviour is guaranteed to be consistent regardless of what the context manager does internally.
✅ Expert tab complete
- I can use ExitStack to manage a dynamic number of context managers
- I can write an async context manager with @asynccontextmanager for use with async with
- I can trace through the exact desugar: __enter__, body execution, __exit__ call sequence