This lesson on Context Managers & Decorators is hands-on and example-driven. You will build custom context managers and function decorators to guarantee resource cleanup and dynamically inject cross-cutting behavior across your codebase. You will implement class-based and generator-based context protocols as well as parameterized decorators without losing function metadata.
What You'll Be Able To Do
- Implement class-based context managers by defining the enter and exit magic methods for deterministic resource cleanup.
- Refactor verbose context managers into concise generator functions using contextlib.contextmanager and yield.
- Create reusable function decorators to intercept arguments, measure execution time, and log return values.
- Construct parameterized decorators using a three-tier nested closure pattern to accept configuration arguments.
- Preserve wrapped function metadata including names and docstrings using functools.wraps.
Detailed Concept Walkthrough
1. Class-Based Context Managers
Context managers encapsulate setup and teardown logic around execution blocks using Python's with statement. They guarantee cleanup even when exceptions interrupt normal execution flow.
- Protocol Mechanics: A class implements the Context Management protocol by defining
__enter__and__exit__. Entering thewithblock executes__enter__and binds any return value to theasvariable. - Exception Handling: The runtime passes
exc_type,exc_value, andtracebackto__exit__. ReturningTruefrom__exit__suppresses the active exception, while returningFalseorNoneallows it to propagate. - Under the Hood: Python translates
with Manager() as m:into an internaltry...finallyblock, ensuring__exit__runs regardless of whether the body succeeds, returns, or crashes.
import time
class Timer:
def __enter__(self):
self.start = time.perf_counter()
return self # Bound to variable in 'as' clause
def __exit__(self, exc_type, exc_val, exc_tb):
self.elapsed = time.perf_counter() - self.start
print(f"Elapsed time: {self.elapsed:.4f}s")
return False # Do not suppress exceptions
with Timer() as t:
sum(i * i for i in range(100_000))
Key Takeaway: Class-based context managers guarantee teardown via
__exit__, which controls exception suppression by returning a boolean.
2. Generator Context Managers via Contextlib
The contextlib.contextmanager decorator converts a generator function into a context manager, eliminating class boilerplate with a single yield statement.
- Execution Flow: Code prior to
yieldexecutes upon entering thewithblock, the yielded value binds to theastarget, and code afteryieldexecutes upon exiting. - Syntax Rule: The
yieldstatement must be wrapped in atry...finallyblock. This ensures teardown code infinallyexecutes even if an unhandled exception occurs inside thewithblock. - Under the Hood: The decorator wraps the generator inside an internal class whose
__enter__callsnext()and whose__exit__resumes execution viagenerator.throw()ornext().
from contextlib import contextmanager
import time
@contextmanager
def timer_context():
start = time.perf_counter()
try:
yield start # Control transfers to with-block body
finally:
# Teardown logic always runs
elapsed = time.perf_counter() - start
print(f"Block executed in {elapsed:.4f}s")
with timer_context():
sum(x for x in range(100_000))
Key Takeaway: Using
@contextmanagerallows concise setup and teardown logic separated by a singleyieldinside atry...finallyblock.
3. Function Decorator Mechanics and Metadata
Decorators are higher-order functions that accept a callable and return a modified wrapper callable without altering the original function's source code.
- Execution Timing: Decorators execute exactly once at module load or definition time (
func = decorator(func)), whereas the returned inner wrapper executes on each invocation. - Wrapping Pattern: The decorator defines an inner wrapper that accepts
*argsand**kwargs, injects pre- or post-processing logic, calls the original function, and returns its result. - Metadata Preservation: Replacing a function with a wrapper masks the original
__name__and__doc__. Applying@functools.wraps(func)to the wrapper copies original metadata back onto it.
import functools
def logger(func):
@functools.wraps(func) # Preserves func.__name__ and docstrings
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__} with args={args}, kwargs={kwargs}")
result = func(*args, **kwargs)
print(f"{func.__name__} returned {result}")
return result
return wrapper
@logger
def compute_total(price, tax=0.05):
"""Calculates total price."""
return price * (1 + tax)
Key Takeaway: Decorators modify callables at definition time and require
@functools.wrapsto prevent losing introspection metadata.
4. Parameterized Decorators
When a decorator needs custom arguments, a three-tier nested function pattern creates an outer factory that generates and returns the actual decorator closure.
- Evaluation Hierarchy: Writing
@decorator_factory(arg)immediately invokesdecorator_factory(arg), which must return a standard decorator expectingfuncas its sole parameter. - Triple-Nesting Architecture: The outermost function captures configuration arguments, the middle function captures the target callable, and the innermost wrapper handles per-call execution.
- Under the Hood: Python evaluates
@permission_required('admin')by resolving the factory call first, resulting indelete_user = permission_required('admin')(delete_user).
import functools
def permission_required(required_role):
# Level 1: Receives decorator arguments
def decorator(func):
# Level 2: Receives target function
@functools.wraps(func)
def wrapper(*args, **kwargs):
# Level 3: Executes per function invocation
current_user_role = kwargs.get("role", "guest")
if current_user_role != required_role:
raise PermissionError(f"Requires {required_role} role")
return func(*args, **kwargs)
return wrapper
return decorator
@permission_required("admin")
def delete_record(record_id, role="guest"):
return f"Record {record_id} deleted"
Key Takeaway: Decorator arguments require an extra outer factory function returning the decorator closure before wrapping the target callable.
Topics Covered in Context Managers & Decorators
- The with Statement and Resource Handling (0:00 - 2:45) — Demonstrates file management using the with statement to guarantee deterministic resource cleanup.
- Class-Based Context Managers (2:45 - 6:10) — Implements a custom Timer class utilizing the enter and exit context protocol methods.
- Function Decorator Syntax (6:10 - 9:30) — Explains decorator execution mechanics at module load time as higher-order function wrappers.
- Building a Logger Decorator (9:30 - 13:55) — Constructs a standard wrapper decorator capturing function arguments and return values.
- Handling Decorator Arguments (13:55 - 17:30) — Applies a triple-nested closure structure to create a role-based permission decorator.
- Generator Contexts with contextlib (17:30 - 20:15) — Refactors class-based context logic into concise generator functions using the contextmanager decorator.
Python Cheat Sheet
-
with resource as target:— Ensures automatic resource entry and cleanupwith open('app.log', 'w') as f: f.write('initialized') -
def __enter__(self):— Executes context setup and returns bound valuedef __enter__(self): self.start = time.time() return self -
def __exit__(self, exc_type, exc_val, tb):— Executes context teardown and handles block exceptionsdef __exit__(self, exc_type, exc_val, tb): return True # Suppresses active exception -
@contextmanager— Converts generator function into a context managerfrom contextlib import contextmanager @contextmanager def sample(): yield 'payload' -
@functools.wraps(func)— Preserves original function name and docstrings@functools.wraps(func) def wrapper(*args, **kwargs): return func(*args, **kwargs) -
@decorator— Wraps function with higher-order function callable@logger def calculate(a, b): return a + b -
@factory(arg)— Passes configuration arguments to decorator closure@permission_required('admin') def purge_data(): pass
Comparison Table
| Pattern / Tool | Implementation Structure | Primary Use Case |
|---|---|---|
| Class-Based Context Manager | Class with __enter__ and __exit__ | Stateful resource cleanup and inspection |
| Generator Context Manager | Generator function using @contextmanager | Concise linear setup and teardown pairs |
| Standard Function Decorator | Two-tier closure (decorator -> wrapper) | Cross-cutting logic like timing and logging |
| Parameterized Decorator | Three-tier closure (factory -> decorator -> wrapper) | Configurable function wrappers needing dynamic parameters |
Common Pitfalls
- Mistake: Placing per-call execution logic directly in the outer decorator body. Avoid: Place per-invocation code inside the inner wrapper function because outer decorator bodies execute once at import.
- Mistake: Defining exit with fewer than three exception arguments. Avoid: Always define exit with self, exc_type, exc_val, and exc_tb parameters.
- Mistake: Omitting functools.wraps on inner wrapper functions. Avoid: Decorate every inner wrapper function with functools.wraps to preserve original function name and docstring introspection.
- Mistake: Omitting try...finally around the yield statement in generator context managers. Avoid: Always wrap yield inside a try...finally block so cleanup executes even during unhandled exceptions.
FAQs
- How do I suppress exceptions raised inside a context manager block?
Return True from the
__exit__method. If it returns False or None, Python propagates the exception automatically. - Why does my decorated function show wrapper as its name?
Decorating replaces the original function object with the inner wrapper. Apply
@functools.wraps(func)to the inner wrapper to retain the original function metadata. - When should I use a class-based context manager over @contextmanager? Use class-based managers when you need state retention across multiple helper methods, and generator context managers for concise procedural teardowns.
- Why do parameterized decorators require three levels of nested functions? The outermost function accepts configuration arguments, the middle receives the target function, and the innermost wrapper executes logic on each invocation.