Python Decorators
A decorator adds behaviour to a function without changing its code: logging, timing, retrying, checking permissions. You meet decorators everywhere in test frameworks, such as @pytest.fixture and @pytest.mark.parametrize. This lesson builds one from scratch.
- Why decorators work: functions are objects
- Write a wrapper and apply it with @
- Keep the function’s name with functools.wraps
- Write decorators that take arguments
Functions are objects
A decorator relies on two facts: a function can be passed to another function, and a function can create and return a new function.
def shout(text): return text.upper() def call_twice(func, value): return func(value), func(value) print(call_twice(shout, "hi"))('HI', 'HI')A decorator by hand
A decorator is a function that takes a function and returns a new function, usually a wrapper that runs extra code around the original:
def log_calls(func): def wrapper(*args, **kwargs): print(f"calling {func.__name__} with {args}") result = func(*args, **kwargs) print(f"{func.__name__} returned {result}") return result return wrapper def add(a, b): return a + b add = log_calls(add) # replace add with the wrapped versionadd(2, 3)calling add with (2, 3) add returned 5
The @ syntax
@log_calls above a definition is a shortcut for add = log_calls(add):
def log_calls(func): def wrapper(*args, **kwargs): print(f"calling {func.__name__}{args}") return func(*args, **kwargs) return wrapper @log_callsdef multiply(a, b): return a * b print(multiply(4, 5))calling multiply(4, 5) 20
The wrapper uses *args, **kwargs so it can pass along whatever arguments the decorated function takes.
Keep the original name with functools.wraps
Without help, the decorated function reports the wrapper’s name and loses its docstring. functools.wraps copies them over:
from functools import wraps def plain(func): def wrapper(*args, **kwargs): return func(*args, **kwargs) return wrapper def proper(func): @wraps(func) def wrapper(*args, **kwargs): return func(*args, **kwargs) return wrapper @plaindef first(): """First docstring.""" @properdef second(): """Second docstring.""" print(first.__name__, first.__doc__)print(second.__name__, second.__doc__)wrapper None second Second docstring.
Always use @wraps in your decorators.
A timing decorator
import timefrom functools import wraps def timer(func): @wraps(func) def wrapper(*args, **kwargs): start = time.perf_counter() result = func(*args, **kwargs) elapsed = time.perf_counter() - start print(f"{func.__name__} took {'under' if elapsed < 0.5 else 'over'} 0.5 s") return result return wrapper @timerdef slow_sum(n): time.sleep(0.1) return sum(range(n)) print(slow_sum(1000))slow_sum took under 0.5 s 499500
Decorators with arguments
To pass settings such as @retry(times=3), add one more outer function. retry(times=3) runs first and returns the actual decorator.
from functools import wraps def retry(times): def decorator(func): @wraps(func) def wrapper(*args, **kwargs): for attempt in range(1, times + 1): try: return func(*args, **kwargs) except ValueError as e: print(f"attempt {attempt} failed: {e}") raise RuntimeError(f"{func.__name__} failed {times} times") return wrapper return decorator calls = {"n": 0} @retry(times=3)def flaky(): calls["n"] += 1 if calls["n"] < 3: raise ValueError("server busy") return "success" print(flaky())attempt 1 failed: server busy attempt 2 failed: server busy success
Stacking decorators
Several decorators apply from the bottom up: the one closest to the function wraps it first.
def bold(func): return lambda: "<b>" + func() + "</b>" def italic(func): return lambda: "<i>" + func() + "</i>" @bold@italicdef label(): return "Python" print(label())<b><i>Python</i></b>
Built-in decorators you will meet
| Decorator | What it does |
|---|---|
@staticmethod, @classmethod | Methods that do not need an instance (see Classes & Objects) |
@property | Call a method like an attribute |
@functools.lru_cache | Remember results of expensive calls |
@dataclasses.dataclass | Generate __init__, __repr__ and more for a class |
@pytest.fixture, @pytest.mark.parametrize | Set up test data and run a test with many inputs |
Check your understanding
Predict the output
def twice(func): def wrapper(x): return func(func(x)) return wrapper @twicedef add3(x): return x + 3 print(add3(10))16
add3 is replaced by wrapper, which calls the original twice: 10 + 3 = 13, then 13 + 3 = 16.
Quick quiz
1. What is @deco above def f(): equivalent to?
The decorator receives the function and its return value replaces it.
2. Why use functools.wraps?
Without it, __name__ and __doc__ come from the wrapper.
3. With @a above @b above def f, which wraps f first?
Decorators apply from the bottom up: f = a(b(f)).
Exercise: count calls
Write a decorator count_calls that counts how many times a function has been called and stores the number in wrapper.calls.
call greet("Asha"), greet("Ravi"), then print greet.callsHello Asha Hello Ravi calls: 2
Hint 1
Set wrapper.calls = 0 before returning the wrapper, and add 1 inside it.
from functools import wraps def count_calls(func): @wraps(func) def wrapper(*args, **kwargs): wrapper.calls += 1 return func(*args, **kwargs) wrapper.calls = 0 return wrapper @count_callsdef greet(name): return f"Hello {name}" print(greet("Asha"))print(greet("Ravi"))print("calls:", greet.calls)Hello Asha Hello Ravi calls: 2
Functions are objects, so the wrapper can carry its own attribute. Each call increments it before calling the original function.
Program Practice
39 Python Function Programs
Every program has a step-by-step explanation, colour-coded code and its real output.
Python decorators FAQ
What is a decorator in Python?
A decorator is a function that takes another function and returns a modified version of it, usually a wrapper that adds behaviour before or after the original runs.
What does functools.wraps do?
It copies the wrapped function’s name, docstring and other metadata onto the wrapper, so debugging and documentation tools see the original function.
Can a decorator take arguments?
Yes. Write a function that takes the arguments and returns the decorator, giving three levels: def retry(times): def decorator(func): def wrapper(...).