Warming up the neural circuits...
By the end of this chapter you will:
Decorators are Python's cleanest tool for cross-cutting behavior. You can add logging, timing, retry, authorization, caching, and without rewriting every function body.
Instead of copy-pasting boilerplate, you wrap behavior once and reuse it everywhere.
def greet(name: str) -> str:
return f"Hello, {name}"
fn = greet
print(fn("Om"))Because functions are objects, you can pass them into wrappers and return enhanced versions.
import time
from functools import wraps
def timer(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs
@timer is syntax sugar for slow_add = timer(slow_add).
Without @wraps, metadata is lost:
__name__ becomes wrapper__doc__ disappearsAlways add @wraps(func) for production decorators.
from functools import wraps
def retry(times: int):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
last_err = None
for _ in range
The pattern is three layers: config -> decorator -> wrapper.
from functools import lru_cache
@lru_cache(maxsize=256)
def normalize_country(code: str) -> str:
return code.strip().upper()Typical production decorators:
Flask routes, FastAPI dependencies, and many pytest helpers rely on decorators. Once the wrapper model is clear, framework internals become much easier to read.
| Need | Use |
|---|---|
| Cross-cutting behavior around many functions | Decorator |
| Behavior that requires mutable object | Class-based callable/decorator |
| One-off logic in one function | Inline code (no decorator needed) |
| Caching pure function outputs | functools.lru_cache |
| Mistake | Symptom | Fix |
|---|---|---|
Forgetting @wraps | Wrong function names/docs in logs | Add @wraps(func) |
| Swallowing all exceptions in retry | Hidden failures and data issues | Limit retries and re-raise with context |
| Decorating impure or side-effectful function with cache | Stale or incorrect output | Cache only pure/deterministic functions |
| Deeply stacked decorators without order reasoning | Unexpected behavior | Document and test decorator order |
| Validation inside every handler manually | Repetition and drift | Centralize validation in reusable decorator |
A function add(a, b)Console log before resultDecorator without wrapsFunction name/doc remain correct@retry(3) that retries a function up to three times.Function that fails intermittentlySuccess on later attempt or final raised erroramount for payment functions.Function with amount argumentRaises ValueError for invalid amountNeed timing across many function boundaries and one custom blockDecorator for functions, context manager for ad-hoc block timingBeginner:
"What does @decorator syntax do under the hood?"
@decorewrites tofn = deco(fn)at definition time.
"Why is functools.wraps important?"
It preserves metadata like function name and docstring, improving debugging and tooling.
Senior:
"How do you avoid overusing decorators?"
Use decorators for policy that repeats across many boundaries; avoid for one-off local logic where explicit code is clearer.
"What risks exist in retry decorators?"
Retrying non- operations can duplicate effects. Retry only safe operations and include bounded attempts with backoff.
@wraps is mandatory for maintainable decorators.Need cross-cutting behavior? -> decorator
Need config on decorator? -> 3-layer pattern
Need metadata intact? -> functools.wraps
Need block-scoped behavior? -> context managerWhat does @timer above a function mean under the hood?