Python Decorators
Also Known As
TL;DR
Explanation
A decorator is a callable that takes a function and returns a modified version. @functools.wraps preserves the original function's name and docstring. Built-in decorators: @property (computed attribute), @staticmethod (no self/cls), @classmethod (receives cls), @functools.cache (memoisation), @functools.lru_cache(maxsize=128). Decorator factories take arguments: @lru_cache(maxsize=256). Class decorators: @dataclass generates __init__, __repr__, __eq__ from class annotations — Python's equivalent of PHP 8.1 readonly classes. Stacking: @app.route('/') @login_required applies inside-out. PHP's equivalent is attribute-based annotations (#[Route]) or middleware patterns — Python decorators are more powerful because they execute arbitrary code, not just metadata.
Diagram
flowchart LR
subgraph Decorator_Mechanics
ORIG[def my_func]
WRAP[def decorator wraps func]
APPLIED[my_func = decorator my_func]
ORIG --> WRAP --> APPLIED
end
subgraph Common_Uses
TIMING[measure_time decorator<br/>log execution duration]
CACHE2[functools.lru_cache<br/>memoize results]
AUTH2[require_auth decorator<br/>check before running]
RETRY2[retry decorator<br/>retry on exception]
end
subgraph Syntax
AT[at symbol decorator<br/>above function definition<br/>syntactic sugar]
end
style APPLIED fill:#238636,color:#fff
style CACHE2 fill:#1f6feb,color:#fff
style AUTH2 fill:#f85149,color:#fff
Common Misconception
Why It Matters
Common Mistakes
- Not using functools.wraps — the decorated function loses its __name__ and __doc__.
- Decorators that do not handle the wrapped function's arguments correctly — use *args, **kwargs.
- Stateful decorators without thread safety — mutable state in a decorator is shared across calls.
- Stacking too many decorators — execution order is bottom-up for application and top-down for stacking.
Code Examples
# Decorator without functools.wraps — loses metadata:
def log_call(func):
def wrapper(*args, **kwargs):
print(f'Calling {func.__name__}')
return func(*args, **kwargs)
return wrapper # wrapper.__name__ is 'wrapper', not 'original'
# Fixed:
import functools
def log_call(func):
@functools.wraps(func)
def wrapper(*args, **kwargs): ...
return wrapper
import functools
def retry(times=3):
def decorator(fn):
@functools.wraps(fn)
def wrapper(*args, **kwargs):
for i in range(times):
try: return fn(*args, **kwargs)
except Exception:
if i == times - 1: raise
return wrapper
return decorator
@retry(times=5)
def fetch_data(): ...