AcademyPython
Decorators in Python
What @decorator actually rewrites, why forgetting functools.wraps breaks introspection, and how a decorator that takes arguments needs a third level of nesting.
3 min readUpdated 3 exercises
What @ actually does
@decorator above a function definition is not special syntax with its own rules — it is one line of ordinary code, rewritten:
@decorator
def greet():
return 'hi'def greet():
return 'hi'
greet = decorator(greet)decorator receives the function object, and whatever it returns is bound to the name greet afterward. That returned value does not have to be a function at all — it can be a class instance, a different function entirely, or the original function unchanged. The only contract is "takes a callable, returns something."
The shape almost every decorator has
def logged(fn):
def wrapper(*args, **kwargs):
print(f'calling {fn.__name__}')
return fn(*args, **kwargs)
return wrapper
@logged
def add(a, b):
return a + bwrapper closes over fn — the same closure mechanic that makes a nested function remember a variable from its enclosing scope after that scope has returned. Every call to add is now really a call to wrapper, which does its own work and then delegates. The args and kwargs collector in wrapper's signature is what lets one decorator wrap a function of any arity, rather than one written for exactly two positional parameters.
The bug every undecorated decorator has
add.__name__ # 'wrapper' — not 'add'
add.__doc__ # None — the original docstring is goneOnce add is reassigned to wrapper, every introspection tool — debuggers, help(), API documentation generators, pickle — sees wrapper's identity, not add's. The fix is one line, and it is the reason every production decorator has it:
from functools import wraps
def logged(fn):
@wraps(fn)
def wrapper(*args, **kwargs):
print(f'calling {fn.__name__}')
return fn(*args, **kwargs)
return wrapperwraps(fn) copies __name__, __doc__ and a few other attributes from fn onto wrapper, so the wrapped function still identifies itself correctly from the outside.
A decorator that takes its own arguments
@logged takes no parentheses because logged itself is the decorator. A decorator written as @retry(times=3) needs one more level of nesting, because retry(times=3) has to run first and hand back the actual decorator:
def retry(times):
def decorator(fn):
@wraps(fn)
def wrapper(*args, **kwargs):
for attempt in range(times):
try:
return fn(*args, **kwargs)
except Exception:
if attempt == times - 1:
raise
return wrapper
return decorator
@retry(times=3)
def fetch():
...Three functions, three jobs: retry captures the configuration, decorator captures the function being wrapped, wrapper runs on every call. Losing track of which level owns which variable is the most common bug in a parametrised decorator — a fix belongs in wrapper, a setting belongs in retry.
Stacking more than one
@logged
@retry(times=3)
def fetch():
...Decorators apply bottom to top: fetch is wrapped by retry first, and the result of that is wrapped by logged second. Calling fetch therefore runs logged's wrapper on the outside, which calls retry's wrapper, which calls the original. Reversing the order changes what happens on a failed attempt — with logged innermost, a retried call would log once per attempt instead of once overall.
Classes can decorate too
Anything callable works as a decorator, including a class whose __call__ method does the wrapping — useful when a decorator needs to keep state across calls, like a counter of how many times a function ran, without the extra closure variable a function-based version would need.
Now practice it
1 challenge · 2 quizzesReading this page is the cheap half. These are the exercises that make you use it.
- Decorators — Call CounterProBuild a call counter decorator — state that has to survive between separate calls to the wrapped function.ChallengeSolve · MediumChallengeSolve · Medium
- Fundamentals · Functions & ScopeFunction and scope questions, including how a decorator changes what a function looks like from outside.QuizTake · MediumQuizTake · Medium
- Functional Programming · itertools & partialCovers functools, the module every non-trivial decorator ends up importing.QuizTake · MediumQuizTake · Medium
Read next
Args and kwargs in Python
How the star and double-star collectors turn extra positional and keyword arguments into a tuple and a dict, and where positional-only and keyword-only parameters fit.
3 min
Generators and Iterators in Python
The iterator protocol every for-loop relies on, what yield actually suspends, and why a generator can process a file too large to fit in memory.
3 min