Decorators
A decorator is a function that wraps another function to extend or modify its behavior, without changing the original function's code. Decorators build directly on closures — a decorator is a closure that remembers the function it wraps.
The `@decorator` Syntax
The @decorator line placed above a function definition is just syntactic sugar. It is exactly equivalent to reassigning the function name to the result of calling the decorator on it.
def shout(func):
def wrapper():
return func().upper() + "!"
return wrapper
@shout
def greet():
return "hello"
print(greet())HELLO!
The @shout line above is identical to writing this manually:
def greet():
return "hello"
greet = shout(greet) # same effect as @shout
print(greet())HELLO!
Writing a Timing Decorator From Scratch
A very common real-world decorator measures how long a function takes to run. Since the wrapped function might take arguments, the inner wrapper needs *args/**kwargs to forward whatever it receives.
import time
def timer(func):
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f"{func.__name__} took {elapsed:.6f}s")
return result
return wrapper
@timer
def slow_sum(n):
return sum(range(n))
total = slow_sum(1_000_000)
print(total)slow_sum took 0.021374s 499999500000
wrapper runs the timing logic, calls the real function through func(*args, **kwargs), and returns its result — the caller of slow_sum never notices anything changed except the extra printed timing line.
Decorators With Arguments (Decorator Factories)
Sometimes you want to configure the decorator itself, e.g. @retry(times=3). This requires an extra layer of nesting: an outer function that takes the decorator's own arguments and returns the actual decorator.
def repeat(times):
def decorator(func):
def wrapper(*args, **kwargs):
result = None
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(times=3)
def say_hi():
print("Hi!")
say_hi()Hi! Hi! Hi!
@repeat(times=3) first calls repeat(times=3), which returns decorator. That decorator is then applied to say_hi, exactly like a normal one-argument decorator. Three nested functions — repeat, decorator, wrapper — is the standard shape for any decorator that itself takes arguments.
Preserving Metadata with `functools.wraps`
There's a subtle problem with the decorators above: the wrapped function loses its original name and docstring, because wrapper — not the original function — is what actually gets bound to the decorated name.
def timer(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@timer
def slow_sum(n):
"""Return the sum of numbers from 0 to n-1."""
return sum(range(n))
print(slow_sum.__name__) # 'wrapper', not 'slow_sum'!
print(slow_sum.__doc__) # None, docstring is lost!wrapper None
import functools
def timer(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@timer
def slow_sum(n):
"""Return the sum of numbers from 0 to n-1."""
return sum(range(n))
print(slow_sum.__name__) # 'slow_sum' — correct!
print(slow_sum.__doc__) # original docstring — correct!slow_sum Return the sum of numbers from 0 to n-1.
Built-in Decorators: A Preview
Python ships several decorators used constantly in object-oriented code. They're covered in full in the classes and OOP section, but it's worth knowing they exist:
Decorator | Purpose |
|---|---|
| Defines a method that doesn't receive |
| Defines a method that receives the class ( |
| Lets a method be accessed like an attribute, without parentheses — useful for computed or validated attributes |
Quick Reference
Concept | Syntax |
|---|---|
Basic decorator |
|
Equivalent without sugar |
|
Decorator factory |
|
Preserve metadata |
|
import functools
def require_login(func):
@functools.wraps(func)
def wrapper(user, *args, **kwargs):
if not user.get("is_logged_in"):
raise PermissionError(f"{user['name']} must log in first.")
return func(user, *args, **kwargs)
return wrapper
@require_login
def view_dashboard(user):
print(f"Welcome to your dashboard, {user['name']}!")
view_dashboard({"name": "Ada", "is_logged_in": True})
view_dashboard({"name": "Grace", "is_logged_in": False})Welcome to your dashboard, Ada! PermissionError: Grace must log in first.