Writing Decorators with functools.wraps
22 min · Intermediate
Need help? Ask a tutor on WhatsApp (opens in a new tab)A decorator adds behaviour, such as logging or checking input, around a function without changing the function's own code. It is a closure that takes a function and returns a new one.
A decorator wraps a function
Decorator is a function that takes a function and returns a new function, usually one that calls the original with extra behaviour before or after it. The @ line applies it.
A decorator is a function that receives a function and returns another function. The @ line above a definition is a short way to apply it.
def shout(func): def wrapper(): return func().upper() return wrapper
@shoutdef greet(): return "hello"
print(greet())Example 1 · A decorator in two forms
def shout(func): def wrapper(): return func().upper() return wrapper
def greet(): return "hello"
greet = shout(greet)print(greet())
@shoutdef farewell(): return "goodbye"
print(farewell())HELLO GOODBYE
What this code does
- The first form calls the decorator by hand; the second uses
@. - Both do the same thing.
@shout above greet means greet = shout(greet). From then on the name greet points to wrapper.
Accept any arguments and return the result
A wrapper must pass on whatever arguments it gets and give back the original result. Use *args and **kwargs so it works with any function.
def log_calls(func): def wrapper(*args, **kwargs): print("calling", func.__name__, args) result = func(*args, **kwargs) print("returned", result) return result return wrapper
@log_callsdef add(a, b): return a + b
add(2, 3)Example 2 · Logging calls and results
from functools import wraps
def log_calls(func): @wraps(func) def wrapper(*args, **kwargs): print(f"calling {func.__name__}{args}") result = func(*args, **kwargs) print(f"{func.__name__} returned {result}") return result return wrapper
@log_callsdef total(price, quantity): return price * quantity
total(250, 4)calling total(250, 4) total returned 1000
What this code does
- The wrapper prints before and after the real call and returns the same value.
*argspasses the positional arguments on without the wrapper knowing them.
If the wrapper forgets return result, the decorated function silently returns None.
functools.wraps keeps the identity
functools.wraps is a decorator for the wrapper function. It copies the original function's name, docstring and other details onto the wrapper and links back through __wrapped__.
A plain wrapper replaces the original name and docstring with its own. functools.wraps copies them back, so help text, logs and debuggers still show the real function.
from functools import wraps
def log_calls(func): @wraps(func) def wrapper(*args, **kwargs): return func(*args, **kwargs) return wrapper
@log_callsdef add(a, b): """Add two numbers.""" return a + b
print(add.__name__, add.__doc__)Example 3 · Counting calls on the wrapper
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 ping(): return "pong"
ping()ping()ping()print(ping.calls)3
What this code does
- A function is an object, so it can carry an attribute such as
calls. - The counter lives on the wrapper and is read from outside as
ping.calls.
Example 4 · A retry decorator
from functools import wraps
def retry(func): @wraps(func) def wrapper(*args, **kwargs): for attempt in range(1, 4): try: return func(*args, **kwargs) except ConnectionError: print("attempt", attempt, "failed") raise ConnectionError("gave up") return wrapper
state = {"n": 0}
@retrydef fetch(): state["n"] += 1 if state["n"] < 3: raise ConnectionError return "data"
print(fetch())attempt 1 failed attempt 2 failed data
What this code does
- The first two calls fail, the third succeeds and its value is returned.
- After three failed attempts the wrapper would raise its own error.
Without @wraps, this prints wrapper None. Always use it in decorators you write.
Stacking decorators
Several decorators can sit on one function. The one closest to the function is applied first, and the top one wraps everything.
def bold(func): return lambda: f"<b>{func()}</b>"
def italic(func): return lambda: f"<i>{func()}</i>"
@bold@italicdef text(): return "hi"
print(text())Example 5 · Order of stacked decorators
def bold(func): return lambda: f"<b>{func()}</b>"
def italic(func): return lambda: f"<i>{func()}</i>"
@bold@italicdef a(): return "hi"
@italic@bolddef b(): return "hi"
print(a())print(b())<b><i>hi</i></b> <i><b>hi</b></i>
What this code does
- The decorator nearest the function wraps first, so it ends up inside.
- Swapping the order swaps the nesting.
This is text = bold(italic(text)). italic wraps first, and bold wraps the result.
Next step
Check your understanding
12 quick questions