Using the Python contextmanager Decorator
python contextmanager decorator: Learn how Python's @contextmanager decorator turns generator functions into context managers for clean resource management and excepti...
python contextmanager decorator requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.
The @contextmanager decorator from Python's contextlib module lets you write a context manager as a generator function instead of a class with __enter__ and __exit__ methods. This is useful when the setup and teardown logic is straightforward and doesn't need the full object-oriented structure.
Core Syntax of the @contextmanager Decorator
The basic pattern looks like this:
from contextlib import contextmanager @contextmanager def managed_resource(): resource = acquire_resource() try: yield resource finally: release_resource(resource)
The function must contain a yield statement. Everything before the yield runs when the with block is entered, and everything after the yield runs when the block exits. The value passed to yield is what the as clause receives on the caller side.
How the yield Statement Controls Execution Flow
The decorator transforms the generator function into an object that satisfies the context manager protocol. When the with statement executes, the generator is started and runs until the yield. The yielded value becomes the result of the with ... as expression.
When the block exits, the generator is resumed. If the block exits normally, execution continues after the yield. If an exception propagates from the block, it is thrown into the generator at the yield point. This is why the try/finally structure matters: the finally block runs regardless of whether the block completed normally or raised.
@contextmanager def temporary_file(path): file = open(path, "w") try: yield file finally: file.close()
Without the try/finally, an exception in the with block would prevent the cleanup code from running, leaking the file handle.
Handling Exceptions Inside the Context Manager
The generator can catch exceptions thrown into it. When an exception propagates from the with block, it is thrown at the yield statement. You can catch it and decide whether to suppress it or re-raise it.
@contextmanager def ignore_value_error(): try: yield except ValueError: pass
If the exception is caught and not re-raised, the with block completes without propagating the error. If you want the exception to propagate, re-raise it explicitly with raise.
One subtlety: if the generator yields and the with block raises, but the generator itself raises a different exception during cleanup, that new exception replaces the original one. This can make debugging harder, so cleanup code should avoid raising unless it has a clear reason.
Returning a Value vs. Yielding
A common mistake is using return instead of yield in a @contextmanager function. The function must contain a yield; using return without yielding makes the decorator raise a RuntimeError when the context manager is entered.
If you need to provide a value to the with ... as clause, yield it. If no value is needed, a bare yield works:
@contextmanager def transaction(): begin() try: yield finally: commit()
The as clause is optional on the caller side, and the yielded value is simply None here.
Class-Based Context Managers vs. @contextmanager
Both approaches satisfy the context manager protocol, but they have different tradeoffs.
| Aspect | Class-based | @contextmanager |
|---|---|---|
| State storage | Instance attributes | Generator local variables |
| Exception handling | __exit__ receives exception details | try/except around yield |
| Reusability | Can be reused if designed for it | Single-use per generator instance |
| Readability | More boilerplate | Less boilerplate for simple cases |
A class-based context manager is necessary when you need to inspect the exception type, value, and traceback separately, or when the same context manager instance must be entered multiple times. The @contextmanager approach is more concise when the setup and teardown are linear.
Note that a @contextmanager generator is single-use. Each with statement needs a fresh call to the function. If you reuse the same generator object, the second with block fails because the generator is already exhausted.
Runtime Cost and Performance Considerations
The decorator adds a small layer of indirection compared to a hand-written class. Each entry involves starting a generator, and each exit involves resuming it. For typical application code, this overhead is negligible compared to the work done inside the block.
The more important consideration is what happens inside the generator. If the setup or teardown performs expensive operations, those dominate the cost. The decorator itself does not add meaningful latency for I/O-bound or resource-management code.
For code that runs in a tight loop with millions of iterations, the generator overhead can become measurable. In that case, a class-based context manager or manual try/finally may be slightly faster, but you should measure before optimizing. The readability benefit of @contextmanager usually outweighs the small runtime difference.
Common Mistakes and Edge Cases
One common mistake is wrapping the yield in a try/except that catches BaseException and then swallowing it. This can hide KeyboardInterrupt or SystemExit, which are rarely intended to be suppressed.
Another edge case: if the with block calls return, break, or continue, the generator is resumed and the cleanup code runs. The finally block in the generator handles these control-flow exits correctly.
When the generator is closed without being fully consumed, Python raises GeneratorExit at the yield. The finally block runs, which is the correct cleanup behavior.
A subtle issue arises when the generator yields a value and the consumer never enters the with block. If you call the function and discard the generator without using it, the setup code never runs. This is usually fine, but it means the resource is never acquired, which is the expected behavior.
When to Use @contextmanager in Production Code
Use @contextmanager when the setup and teardown logic fits in a single function and the context manager does not need to persist state between uses. Common examples include database transactions, file handles, temporary directory changes, and locking primitives.
Prefer a class-based context manager when you need to expose additional methods on the context manager object, when the __exit__ logic needs fine-grained control over exception propagation, or when the same context manager instance must be entered multiple times.
The decorator is also useful for converting an existing generator function into a context manager without rewriting it as a class. This keeps the code close to the original logic and reduces the diff when refactoring.