What Is a Decorator?
A decorator is a function that wraps another function, adding behaviour before or after it runs — without modifying the original function's code. Decorators are used everywhere in Python: Flask uses them to define routes, Django uses them for login protection, and the standard library uses them for caching and property definitions.
At its core, a decorator is just a callable that takes a function and returns a new function.
The Building Block: Higher-Order Functions
Before decorators make sense, you need to understand that in Python, functions are first-class objects — they can be passed as arguments and returned from other functions:
def shout(func):
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return str(result).upper()
return wrapper
def greet(name):
return f"hello, {name}"
louder_greet = shout(greet)
print(louder_greet("alice")) # HELLO, ALICE
The @ Syntax
The @decorator syntax is just syntactic sugar for the above pattern:
@shout
def greet(name):
return f"hello, {name}"
# This is exactly equivalent to:
# greet = shout(greet)
Preserving the Original Function's Identity
Without extra care, wrapping a function hides its name and docstring. Always use functools.wraps to preserve function metadata:
import functools
def shout(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return str(result).upper()
return wrapper
Real-World Use Case 1: Timing Functions
import time
import functools
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__} ran in {elapsed:.4f}s")
return result
return wrapper
@timer
def slow_operation():
time.sleep(0.5)
return "done"
Real-World Use Case 2: Retry Logic
import functools, time
def retry(times=3, delay=1.0):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(1, times + 1):
try:
return func(*args, **kwargs)
except Exception as e:
if attempt == times:
raise
print(f"Attempt {attempt} failed: {e}. Retrying...")
time.sleep(delay)
return wrapper
return decorator
@retry(times=3, delay=0.5)
def fetch_data(url):
# ... network call that might fail
pass
Notice how this decorator takes arguments — it's a "decorator factory" that returns the actual decorator.
Real-World Use Case 3: Caching with functools.lru_cache
Python's standard library includes a powerful caching decorator out of the box:
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, even for large n
Class-Based Decorators
You can also implement decorators as classes by defining __call__:
class CountCalls:
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"Call #{self.count} to {self.func.__name__}")
return self.func(*args, **kwargs)
When to Use Decorators
- Cross-cutting concerns: logging, timing, authentication, caching
- Input/output transformation: type checking, serialization
- Framework integration: registering routes, signal handlers
Decorators are one of Python's most expressive features. Once you understand the pattern, you'll start recognizing opportunities to use them everywhere — and your code will be cleaner for it.