Question bank — Decorators — function decorators, @, wraps
These traps live at the seams between First-class functions, Closures and free variables, Higher-order functions and args and kwargs unpacking. If a card feels shaky, follow the wikilink.
True or false — justify
The @ symbol adds new syntax capability that plain Python cannot express.
@deco is pure sugar for f = deco(f); anything a decorator does you can write by hand with an assignment.A decorator must always return a new function.
func unchanged is legal but adds nothing.@deco and @deco() are interchangeable.
@deco passes the target function to deco; @deco() first calls deco() with no args and decorates with its return value. Only factory decorators work with the parentheses form.Using functools.wraps changes what the wrapped function computes.
wraps only copies metadata (__name__, __doc__, etc.); the runtime behaviour and return value are untouched.A closure is required for a decorator to work.
func after deco returns, and that memory is exactly a closure over the free variable func (see Closures and free variables).Stacked decorators @a over @b apply top-down (a first).
f = a(b(f)), so b (nearest the def) is applied first, then a wraps the result.@repeat(3) and @repeat behave the same if repeat is written as a factory.
@repeat passes the function directly to the factory (so func becomes the "n"), producing broken or wrong behaviour; the factory needs to be called first as @repeat(3).Decorating a function twice with the same decorator double-wraps it.
@deco over @deco gives f = deco(deco(f)), so the added behaviour runs twice, nested.@wraps must decorate the outer decorator function.
Spot the error
def timer(func):
def wrapper(*a, **k):
func(*a, **k)
return wrapper::: The wrapper never returns func(*a, **k), so the decorated function silently yields None — any caller doing x = f(...) loses the real result.
def deco(func):
def wrapper():
return func()
return wrapper::: The wrapper takes no arguments, so decorating any function that expects parameters raises TypeError. Fix with def wrapper(*args, **kwargs) and forward them (see args and kwargs unpacking).
def deco(func):
return func::: It "works" and errors nothing, but it adds no behaviour — it hands back the unchanged function. You almost certainly meant to build and return an inner wrapper.
@repeat
def hi(): print("hi")::: If repeat is a parametrised factory, this passes hi as the count n and returns a decorator that never gets applied — you must write @repeat(3).
def deco(func):
def wrapper(*a, **k):
return func(*a, **k)
# (no return here)::: deco forgets to return wrapper, so decorating rebinds the name to None (deco's implicit return) and calling f() raises TypeError: 'NoneType' object is not callable.
from functools import wraps
def deco(func):
@wraps
def wrapper(*a, **k): return func(*a, **k)
return wrapper::: @wraps must be called with the original function: @wraps(func). Writing bare @wraps tries to wrap the wrapper with wraps itself and fails.
@a @b
def f(): ...::: Two decorators cannot share one line; each @ needs its own line stacked above the def. Write @a then @b on separate lines.
Why questions
Why does the wrapper use *args, **kwargs instead of the target's real parameters?
Why do parametrised decorators need three nested functions?
n), layer 2 captures the function (func), layer 3 does the work — each closure adds one thing the next layer must remember.Why is @wraps recommended even when your code "runs fine"?
f.__name__ becomes 'wrapper' and __doc__ is lost, breaking help(), logging, tracebacks and any tool that introspects the function.Why can decorators be stacked at all?
Why is returning result (not just calling func) essential?
None and the function's output vanishes.Why must deco(arg) return a decorator for @deco(arg) to work?
f = deco(arg)(f); deco(arg) is called first and its result is applied to f, so that result must itself be a function-taking-function.Why is a decorator called a "higher-order function"?
Edge cases
What happens if you decorate a function that returns None on purpose?
None; only forgetting the return (accidentally producing None) is a bug, not deliberate None.What does f.__wrapped__ point to after @wraps?
wraps sets it so tools can "unwrap" and reach the true target.Can a decorator swallow or transform exceptions raised by func?
func(*a, **k) in try/except to retry, log, or re-raise. That's a common real use (retries).What is type(f) after applying a plain function decorator?
function — because the wrapper is an ordinary inner function. But if the decorator returns a class instance, f becomes that object type instead.Does a decorator run at definition time or call time?
def/import time; the returned wrapper runs its extra logic each time the function is called.What if two stacked decorators both use @wraps(func)?
@wraps, introspection survives the whole stack.Can you decorate a method inside a class the same way as a function?
self as its first positional arg — which *args already handles. Descriptor-based cases belong to Class decorators and property.What happens if the factory @repeat(0) is used (n = 0)?
for _ in range(0) never runs, so result is never assigned and returning it raises UnboundLocalError — a boundary the naive implementation forgets.Connections
- Closures and free variables
- First-class functions
- functools — wraps, lru_cache, partial
- Higher-order functions
- Class decorators and property
- args and kwargs unpacking