A decorator is just a function that takes a function and returns a function. Once that clicks, everything else follows — argument decorators, class decorators, stacking, and the production patterns used in every major Python framework. This guide builds from first principles to real production patterns.
⚡ TL;DR: Always use @functools.wraps inside your wrapper to preserve metadata. Use decorator factories for configurable decorators. Stack decorators bottom-up. Common uses: timing, caching, retry, auth, validation.
What a decorator actually is
import functools
def timer(fn):
@functools.wraps(fn) # Preserves __name__, __doc__
def wrapper(*args, **kwargs):
import time
start = time.perf_counter()
result = fn(*args, **kwargs)
ms = (time.perf_counter() - start) * 1000
print(f"{fn.__name__} took {ms:.1f}ms")
return result
return wrapper
@timer # Same as: greet = timer(greet)
def greet(name):
return f"Hello, {name}"
greet("Alice") # greet took 0.01ms
Decorator factories (decorators with arguments)
import functools, time
def retry(max_attempts=3, delay=1.0, exceptions=(Exception,)):
def decorator(fn):
@functools.wraps(fn)
def wrapper(*args, **kwargs):
last_exc = None
for attempt in range(1, max_attempts + 1):
try:
return fn(*args, **kwargs)
except exceptions as e:
last_exc = e
if attempt < max_attempts:
time.sleep(delay * attempt)
raise last_exc
return wrapper
return decorator
@retry(max_attempts=5, delay=0.5, exceptions=(ConnectionError,))
def call_api(url: str) -> dict:
pass
# Stacking (applied bottom-up):
@timer # Second (outer)
@retry(3, 1.0) # First (inner)
def fetch_data(url):
pass
Built-in caching decorators
from functools import lru_cache, cache
@cache # Python 3.9+ unbounded
def fibonacci(n):
if n < 2: return n
return fibonacci(n-1) + fibonacci(n-2)
@lru_cache(maxsize=128) # Bounded LRU
def expensive_query(user_id: int, page: int):
pass # Result cached per (user_id, page) combo
# Class-based decorator: singleton
def singleton(cls):
instances = {}
@functools.wraps(cls)
def get(*args, **kwargs):
if cls not in instances:
instances[cls] = cls(*args, **kwargs)
return instances[cls]
return get
@singleton
class DatabasePool:
def __init__(self): self.connections = []
Real production decorator patterns
- ✅ @timer — log execution time for monitoring
- ✅ @retry(attempts=3) — retry flaky external calls
- ✅ @lru_cache — memoize expensive pure functions
- ✅ @require_auth — validate JWT before route handler (Flask/FastAPI)
- ✅ @rate_limit(100, 60) — throttle function calls
- ❌ Never forget @functools.wraps — loses function metadata
- ❌ Never use decorators for hidden side effects callers cannot see
Decorators power the Python performance optimization — @lru_cache is one of the most impactful single-line wins. External reference: Python functools documentation.
Recommended Reading
→ Designing Data-Intensive Applications — The essential book every senior developer needs.
→ The Pragmatic Programmer — Timeless engineering wisdom for writing better code.
Affiliate links. We earn a small commission at no extra cost to you.
Free Weekly Newsletter
🚀 Don’t Miss the Next Cheat Code
Join 1,000+ senior developers getting expert JS, Python, AWS and system design secrets weekly.
Discover more from CheatCoders
Subscribe to get the latest posts sent to your email.
