1.3.8 · D5Python Intermediate

Question bank — Decorators — function decorators, @, wraps

1,286 words6 min readBack to topic

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.
False — @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.
False — it must return a callable; that can be the same object, a different function, or even a class instance. Returning func unchanged is legal but adds nothing.
@deco and @deco() are interchangeable.
False — @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.
False — wraps only copies metadata (__name__, __doc__, etc.); the runtime behaviour and return value are untouched.
A closure is required for a decorator to work.
True in practice — the inner wrapper must "remember" 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).
False — they desugar bottom-up to 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.
False — @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.
True — @deco over @deco gives f = deco(deco(f)), so the added behaviour runs twice, nested.
@wraps must decorate the outer decorator function.
False — it decorates the inner wrapper, because the wrapper is the object whose metadata is wrong and needs the original's identity copied onto it.

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?
So one decorator works for every function signature — it forwards whatever arguments arrive unchanged rather than hard-coding a fixed parameter list.
Why do parametrised decorators need three nested functions?
Layer 1 captures the parameter (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"?
Without it, 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?
Because a decorator returns a callable, and that callable is itself a valid target for the next decorator — the output of one feeds into the next.
Why is returning result (not just calling func) essential?
The wrapper replaces the function, so it must relay the original's return value; otherwise callers get None and the function's output vanishes.
Why must deco(arg) return a decorator for @deco(arg) to work?
The desugar is 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"?
Because it takes a function as input and returns a function as output — the defining trait of a higher-order function.

Edge cases

What happens if you decorate a function that returns None on purpose?
Nothing breaks — the wrapper faithfully forwards None; only forgetting the return (accidentally producing None) is a bug, not deliberate None.
What does f.__wrapped__ point to after @wraps?
To the original undecorated function — wraps sets it so tools can "unwrap" and reach the true target.
Can a decorator swallow or transform exceptions raised by func?
Yes — since the wrapper controls the call, it can wrap 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?
Usually 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?
The decorator itself runs once, at 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)?
Each copies metadata from its immediate target, so the outermost wrapper ends up carrying the original name correctly — provided every layer uses @wraps, introspection survives the whole stack.
Can you decorate a method inside a class the same way as a function?
Yes for the wrapping mechanics, but the wrapper must accept 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)?
The loop for _ in range(0) never runs, so result is never assigned and returning it raises UnboundLocalError — a boundary the naive implementation forgets.

Connections