A function decorated with @contextmanager runs its code up to the yield statement as the setup phase, equivalent to __enter__, whatever it yields becomes the value bound by `as` in the with statement, and the code after yield runs as the teardown phase, equivalent to __exit__, once the with block finishes, including if an exception was raised inside the block, which shows up as an exception raised at the yield point itself, letting you wrap it in a try/finally to guarantee cleanup either way. This is often a much more concise way to write a simple context manager than defining a full class with two separate dunder methods.
1Understanding Context Managers (@contextmanager)
A function decorated with @contextmanager runs its code up to the yield statement as the setup phase, equivalent to __enter__, whatever it yields becomes the value bound by as in the with statement, and the code after yield runs as the teardown phase, equivalent to __exit__, once the with block finishes, including if an exception was raised inside the block, which shows up as an exception raised at the yield point itself, letting you wrap it in a try/finally to guarantee cleanup either way. This is often a much more concise way to write a simple context manager than defining a full class with two separate dunder methods.
Wrap the yield in a try/finally inside a @contextmanager function whenever cleanup must happen even if the with block raises an exception — without it, an exception in the block skips your teardown code entirely.
from contextlib import contextmanager
@contextmanager
def announce(name):
print(f"Starting {name}")
yield
print(f"Finished {name}")
with announce("task"):
print("Doing work")2Practical Example
Here is a real-world application of Context Managers (@contextmanager) showing how it is used in production Python code.
from contextlib import contextmanager
import time
@contextmanager
def timer():
start = time.time()
try:
yield
finally:
print(f"Took {time.time() - start:.2f}s")
with timer():
sum(range(1000000))3Best Practices
Follow these guidelines when working with Context Managers (@contextmanager):
1. Use @contextmanager for straightforward setup/teardown logic instead of writing a full class with __enter__/__exit__
2. Wrap the yield in try/finally when cleanup needs to run even if the with block's code raises an exception
3. Reach for a full class-based context manager instead when you need more state or behavior than a single generator function can naturally express
Tip: Wrap the yield in a try/finally inside a @contextmanager function whenever cleanup must happen even if the with block raises an exception — without it, an exception in the block skips your teardown code entirely.
from contextlib import contextmanager
@contextmanager
def announce(name):
print(f"Starting {name}")
yield
print(f"Finished {name}")
with announce("task"):
print("Doing work")