What Is a Python Decorator?
A Python decorator is a design pattern that lets you add new behavior to an existing function — without touching the function’s code. In Python, functions are first-class objects: they can be passed as arguments, assigned to variables, and returned from other functions. Decorators exploit this property.
📋 Table of Contents
The @ syntax is just syntactic sugar. When you write:
@my_decorator
def greet(name):
return f"Hello, {name}!"
Python actually executes:
greet = my_decorator(greet)
The decorator takes the original function, wraps it in a new function with extra behavior, and returns it. The name greet now points to the enhanced version.
Your First Decorator: Step by Step
Let’s build a simple logging decorator that prints a message before and after calling any function.
def log_calls(func):
"""Decorator that logs function entry and exit."""
def wrapper(*args, **kwargs):
print(f"→ Calling {func.__name__} with args={args}, kwargs={kwargs}")
result = func(*args, **kwargs)
print(f"← {func.__name__} returned {result!r}")
return result
return wrapper
@log_calls
def add(a, b):
return a + b
add(3, 4)
# Output:
# → Calling add with args=(3, 4), kwargs={}
# ← add returned 7
Breaking this down:
log_callsreceivesfunc(the original function)- It defines an inner
wrapperthat accepts*args, **kwargs— so it works with any function signature - The wrapper calls the original
func, captures the result, and returns it log_callsreturns the wrapper — not the result of calling it
The functools.wraps Fix
There’s a subtle bug in the decorator above. After decoration, add.__name__ returns 'wrapper', not 'add'. This breaks help(), inspect, and stack traces. Fix it with functools.wraps:
import functools
def log_calls(func):
@functools.wraps(func) # ← copies __name__, __doc__, __module__, etc.
def wrapper(*args, **kwargs):
print(f"→ Calling {func.__name__}")
result = func(*args, **kwargs)
print(f"← {func.__name__} returned {result!r}")
return result
return wrapper
@log_calls
def add(a, b):
"""Add two numbers."""
return a + b
print(add.__name__) # 'add' ✅
print(add.__doc__) # 'Add two numbers.' ✅
@functools.wraps(func) inside every decorator you write. Skipping it causes mysterious bugs in logging, testing frameworks, and API documentation tools.
Decorators with Arguments
What if you want to configure your decorator? For example, @retry(times=3). You need an extra layer of nesting — a decorator factory:
import functools, time
def retry(times=3, delay=1.0, exceptions=(Exception,)):
"""Retry a function up to `times` times on specified exceptions."""
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
last_err = None
for attempt in range(1, times + 1):
try:
return func(*args, **kwargs)
except exceptions as e:
last_err = e
print(f" Attempt {attempt}/{times} failed: {e}")
if attempt < times:
time.sleep(delay)
raise last_err
return wrapper
return decorator
@retry(times=3, delay=0.5, exceptions=(ConnectionError,))
def fetch_data(url):
# simulate a flaky network call
raise ConnectionError("timeout")
try:
fetch_data("https://api.example.com/data")
except ConnectionError:
print("All retries exhausted.")
The three-level structure: retry() → decorator() → wrapper(). Call it: retry(times=3) returns decorator, which takes func and returns wrapper.
Stacking Multiple Decorators
You can apply multiple decorators to a single function. They apply bottom-up (innermost first):
import functools, time
def timer(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f"{func.__name__} took {elapsed:.4f}s")
return result
return wrapper
def log_calls(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
return func(*args, **kwargs)
return wrapper
@timer # applied second (outer)
@log_calls # applied first (inner)
def process(n):
return sum(range(n))
process(1_000_000)
# Output:
# Calling process
# process took 0.0312s
Equivalent to: process = timer(log_calls(process)). Order matters — outer decorators wrap inner ones.
Class-Based Decorators
Any callable object can be a decorator. Using a class gives you state between calls:
import functools
class CallCounter:
"""Counts how many times a function is called."""
def __init__(self, func):
functools.update_wrapper(self, func)
self.func = func
self.count = 0
def __call__(self, *args, **kwargs):
self.count += 1
print(f"{self.func.__name__} called {self.count} time(s)")
return self.func(*args, **kwargs)
@CallCounter
def say_hello(name):
print(f"Hello, {name}!")
say_hello("Alice") # called 1 time(s)
say_hello("Bob") # called 2 time(s)
print(say_hello.count) # 2
@staticmethod vs @classmethod vs @property
Python ships with several built-in decorators you'll use constantly:
class Temperature:
_kelvin_offset = 273.15
def __init__(self, celsius):
self._celsius = celsius
@property
def fahrenheit(self):
"""Computed attribute — no () needed when accessing."""
return self._celsius * 9/5 + 32
@fahrenheit.setter
def fahrenheit(self, value):
self._celsius = (value - 32) * 5/9
@classmethod
def from_kelvin(cls, kelvin):
"""Factory method — creates instance from Kelvin."""
return cls(kelvin - cls._kelvin_offset)
@staticmethod
def is_valid_celsius(value):
"""Utility — no access to instance or class."""
return value >= -273.15
t = Temperature(100)
print(t.fahrenheit) # 212.0 (property)
t.fahrenheit = 32 # setter
t2 = Temperature.from_kelvin(373.15) # classmethod
print(Temperature.is_valid_celsius(-300)) # False (staticmethod)
| Decorator | First Arg | Access | Use Case |
|---|---|---|---|
@property |
self |
Instance + class | Computed attributes, getters/setters |
@classmethod |
cls |
Class only | Factory methods, alternate constructors |
@staticmethod |
None | Neither | Utility functions namespaced to class |
Real-World Use Cases
Decorators power most Python frameworks. Here are production-grade patterns:
1. Caching with LRU Cache
from functools import lru_cache
@lru_cache(maxsize=128)
def fibonacci(n):
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
print(fibonacci(50)) # Instant — cached results
print(fibonacci.cache_info()) # CacheInfo(hits=48, misses=51, ...)
2. Authentication Guard (Flask/FastAPI style)
import functools
def require_auth(func):
"""Decorator for protected routes."""
@functools.wraps(func)
def wrapper(request, *args, **kwargs):
token = request.headers.get("Authorization", "")
if not token.startswith("Bearer "):
return {"error": "Unauthorized"}, 401
return func(request, *args, **kwargs)
return wrapper
@require_auth
def get_user_profile(request):
return {"user": "Alice", "email": "alice@example.com"}
3. Rate Limiter
import functools, time
from collections import defaultdict, deque
def rate_limit(calls=10, period=60):
"""Allow at most `calls` per `period` seconds per caller."""
history = defaultdict(deque)
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
now = time.time()
key = args[0] if args else "default" # use first arg as caller ID
q = history[key]
while q and now - q[0] > period:
q.popleft()
if len(q) >= calls:
raise RuntimeError(f"Rate limit exceeded: {calls} calls per {period}s")
q.append(now)
return func(*args, **kwargs)
return wrapper
return decorator
@rate_limit(calls=5, period=60)
def send_email(user_id, message):
print(f"Email sent to {user_id}: {message}")
Common Mistakes to Avoid
- Forgetting
@functools.wraps— breakshelp(), tracebacks, and testing tools - Calling the function in the decorator body —
return wrapper()instead ofreturn wrapper - Not using
*args, **kwargs— breaks any function with parameters - Mutable default state shared across calls — use a closure variable, not a module-level mutable default
- Missing
return resultin wrapper — silently swallows the function's return value
# ❌ Wrong — returns None
def broken(func):
def wrapper(*args, **kwargs):
func(*args, **kwargs) # no return!
return wrapper
# ✅ Correct
def fixed(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs) # always return
return wrapper
🚀 Level Up Your Python Skills
Now that you understand decorators, explore how they power frameworks like structuring Python projects and advanced patterns. Check our guide on Python shorthand expressions for more Pythonic code tricks.
Frequently Asked Questions
What is a Python decorator?
A Python decorator is a function that takes another function as input and returns an enhanced version — without modifying the original. The @ syntax is shorthand for func = decorator(func).
When should I use decorators?
Use decorators for cross-cutting concerns: logging, caching, auth, rate limiting, timing, input validation — any behavior you want to apply to multiple functions without repeating code.
What does functools.wraps do?
It copies the original function's __name__, __doc__, and other attributes to the wrapper, preserving identity for debugging, help(), and testing tools.
Can decorators have arguments?
Yes. Add an outer factory function that accepts the arguments and returns the real decorator. Pattern: @retry(times=3) → retry() returns the decorator.
What's the difference between @staticmethod and @classmethod?
@staticmethod gets no implicit first argument — it's a plain function in a class namespace. @classmethod receives the class (cls) as the first argument, useful for factory methods.
How do stacked decorators work?
Bottom-up: the decorator closest to the def is applied first. @A @B def f() equals f = A(B(f)).
📚 You might also like
🔗 Share this article




✍️ Leave a Comment