Decorators with arguments & functools.wraps
Write a parameterized, well-behaved decorator that preserves the wrapped function.
Decorators that take arguments
Real decorators almost always need configuration. @retry(times=3), @lru_cache(maxsize=128), and Flask's @app.route("/users") all take arguments, because you cannot hardcode a retry count or a route into the decorator itself. A decorator that accepts arguments is called a decorator factory, and it is the pattern you reach for whenever the behavior you are adding needs to be tuned per function.
The three-layer model
A plain decorator is one function: it takes fn and returns a replacement. A parameterized decorator adds one outer layer that takes the config and returns that plain decorator. So there are three nested callables, each taking a different thing:
def multiply_by(factor): # factory: takes the config
def decorator(fn): # decorator: takes the function
def wrapper(*args, **kwargs): # wrapper: takes the call
return fn(*args, **kwargs) * factor
return wrapper
return decorator
Read @multiply_by(3) written above add as two steps:
add = multiply_by(3)(add)
# step 1: multiply_by(3) runs and returns `decorator`
# step 2: decorator(add) runs and returns `wrapper`, rebound to the name `add`
| Layer | Takes | Returns | Runs |
|---|---|---|---|
| multiply_by(factor) | the config, 3 | the decorator | once, at the @ line |
| decorator(fn) | the function being decorated | the wrapper | once, at the @ line |
| wrapper(*args, **kwargs) | the call arguments | fn's result, times factor | on every call |
factor stays reachable inside wrapper because wrapper is a closure over the factory's scope. *args, **kwargs in wrapper forwards any call signature through untouched, so multiply_by works on add, on a one-argument function, or on anything else.
functools.wraps
wrapper is a brand-new function object, so by default it reports itself, not the function it replaced:
print(add.__name__) # 'wrapper' (wrong: this corrupts logs, help(), and tracebacks)
@functools.wraps(fn) copies fn's __name__, __qualname__, __doc__, and __module__ onto wrapper, updates wrapper.__dict__, and sets wrapper.__wrapped__ = fn so introspection tools can still find the original. With it in place, the demo below prints add, which is exactly what the second print verifies. Add functools.wraps to every real decorator you write.
Pitfall: forgetting the parentheses
A factory must be called. @multiply_by(3) is correct; @multiply_by is not. If you drop the (3), Python runs multiply_by(add), binds factor = add, and replaces add with the inner decorator. Nothing fails at definition time, so the bug hides until the next call:
@multiply_by # missing (3)
def add(a, b):
return a + b
add(2, 3) # TypeError: multiply_by.<locals>.decorator() takes 1 positional argument but 2 were given
The plain @name form (no call) is only for decorators that take a function directly. Anything parameterized needs the call.
Interview nuance: know when each layer runs. The factory and the decorator run exactly once, at definition time, when @multiply_by(3) is evaluated. Only wrapper runs on every call. So expensive setup (compiling a regex, opening a connection) belongs in the outer layers where it happens once, not inside wrapper. Each call to multiply_by also creates a fresh scope, so multiply_by(3) and multiply_by(5) capture independent factor values. The classic late-binding closure bug, where every closure shares one variable, only bites when you reuse the same name, for example building decorators inside a for loop.
import functools
def multiply_by(factor):
def decorator(fn):
@functools.wraps(fn)
def wrapper(*args, **kwargs):
return fn(*args, **kwargs) * factor
return wrapper
return decorator
@multiply_by(3)
def add(a, b):
return a + b
print(add(2, 3)) # 15
print(add.__name__) # addApply
Your turn
The task this lesson builds to.
Warm-up (one file): implement multiply_by(factor), a decorator factory that multiplies a
function's result by factor. It's applied to add below, and the run driver calls it.
run(2, 3) should be 15 (because (2 + 3) * 3).
3 hints and 3 automated checks are waiting in the workspace.
Practice
Make it stick
A second problem on the same idea, so it survives past today.
Implement multiply_by(factor) in decorators/wrappers.py: a decorator factory whose wrapper
multiplies the wrapped function's result by factor, using functools.wraps to preserve the
function's name. The read-only math_ops.py applies it. Some tests are hidden.
3 hints and 2 automated checks are waiting in the workspace.