The best in-depth guide on decorators. Read sections 1–4 for the essentials. Highly recommended primary source for this topic.
Decorators are built on one simple fact: functions are objects in Python — just like in JavaScript. You already know this from JS.
// Functions are values
const greet = function(name) {
return `Hello, ${name}`;
};
// Pass as argument
function applyTwice(fn, x) {
return fn(fn(x));
}
// Return a function (factory)
function multiplier(n) {
return (x) => x * n;
}
const double = multiplier(2);
double(5); // 10
def greet(name):
return f"Hello, {name}"
# Functions are objects
print(type(greet)) # <class 'function'>
print(greet.__name__) # "greet"
# Pass as argument
def apply_twice(fn, x):
return fn(fn(x))
# Return a function (factory / closure)
def multiplier(n):
def inner(x):
return x * n
return inner
double = multiplier(2)
double(5) # 10
A decorator is a function that wraps another function to add behavior before or after it runs — without modifying the original. It's the same pattern as higher-order components in React or Express middleware.
def my_decorator(func):
def wrapper(*args, **kwargs):
print("Before the function")
result = func(*args, **kwargs) # call original
print("After the function")
return result
return wrapper
# Manual application
def say_hello():
print("Hello!")
say_hello = my_decorator(say_hello) # wrap it
say_hello()
# Before the function
# Hello!
# After the function
# Syntactic sugar — the @ syntax does exactly the same
@my_decorator
def say_hello():
print("Hello!")
@my_decorator above a function is exactly equal to say_hello = my_decorator(say_hello) below it. The @ just makes it cleaner to read.
Without @wraps, the decorated function loses its identity — its __name__, __doc__, etc. become those of the wrapper. Always use it.
from functools import wraps
def timer(func):
@wraps(func) # ← copy metadata from func → wrapper
def wrapper(*args, **kwargs):
import time
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
@timer
def compute():
"""Expensive computation."""
return sum(range(10_000_000))
print(compute.__name__) # "compute" ✅ (not "wrapper")
print(compute.__doc__) # "Expensive computation." ✅
To pass arguments to a decorator, you need one more level of nesting — a factory that returns the decorator.
def repeat(n):
"""Decorator factory — returns a decorator"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for _ in range(n):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(3)
def greet(name):
print(f"Hello, {name}!")
greet("Alice")
# Hello, Alice!
# Hello, Alice!
# Hello, Alice!
# This is equivalent to:
greet = repeat(3)(greet)
from functools import wraps
def log_calls(func):
@wraps(func)
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
def retry(times=3, exceptions=(Exception,)):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(times):
try:
return func(*args, **kwargs)
except exceptions as e:
if attempt == times - 1:
raise
print(f"Attempt {attempt+1} failed: {e}. Retrying...")
return wrapper
return decorator
@retry(times=3, exceptions=(ConnectionError,))
def fetch_data(url):
# might fail due to network issues
...
from functools import lru_cache
@lru_cache(maxsize=128)
def expensive(n):
return sum(range(n))
# Or manual cache
def memoize(func):
cache = {}
@wraps(func)
def wrapper(*args):
if args not in cache:
cache[args] = func(*args)
return cache[args]
return wrapper
def require_auth(func):
@wraps(func)
def wrapper(request, *args, **kwargs):
if not request.user.is_authenticated:
raise PermissionError("Login required")
return func(request, *args, **kwargs)
return wrapper
# Common in web frameworks like Flask/Django
@require_auth
def dashboard(request):
return render("dashboard.html")
class MyClass:
@staticmethod
def utility():
"""No self or cls — just a namespaced function"""
return 42
@classmethod
def from_string(cls, s):
"""Gets cls (the class) instead of self"""
return cls(int(s))
@property
def computed(self):
"""Called as obj.computed — not obj.computed()"""
return self._val * 2
@computed.setter
def computed(self, value):
self._val = value // 2
| Decorator | Where | Purpose |
|---|---|---|
@staticmethod | Class | Utility with no class/instance access |
@classmethod | Class | Alternative constructors; gets cls |
@property | Class | Computed getter (called without ()) |
@lru_cache | Any function | Memoization with LRU eviction |
@dataclass | Class | Auto-generate __init__, __repr__, __eq__ |
1. What does @my_decorator above a function definition do?
2. Why should you use @wraps(func) inside a decorator?
3. How do you make a decorator that accepts its own arguments (like @repeat(3))?
4. @property lets you call a method as obj.value instead of obj.value(). True or false?
Questions answered correctly.