Run it
The metadata before and after, the wraps fix, and a decorator with an argument so the three layers are visible.
What the @ line replaces, and what that costs.
functools.wraps copies the metadata across, which is what makes the signature survive.
A decorator taking an argument needs three layers - one per thing it receives.
It is one assignment
There is no special decorator machinery. These two are the same program:
@shout
def greet(name): ...# is exactly def greet(name): ... greet = shout(greet)
Which tells you three things immediately. It runs once, when the def is executed, not on every call. The decorator receives the function object and can return literally anything — a different function, a class instance, or a string, though the last one will confuse everybody. And stacked decorators apply bottom-up: the one nearest the def wraps first.
Why wraps exists
If the decorator returns a new function, the name now refers to that function, and it has its own __name__, its own empty __doc__ and its own signature. Everything that reads those breaks quietly: help() becomes useless, documentation tools generate entries for a function called wrapper, and test frameworks that discover by name misbehave.
functools.wraps is a decorator for your wrapper that copies __name__, __doc__, __module__, __qualname__ and __dict__ across, and sets __wrapped__ to the original. That last one is what lets inspect.signature report the real signature rather than (*args, **kwargs).
Decorators that take arguments
@repeat(3) needs one more layer, and the reason follows from the assignment rule. @X calls X(fn); so if X is written as repeat(3), then repeat(3) must itself return a decorator:
def repeat(times): # takes the argument
def decorator(fn): # takes the function
@functools.wraps(fn)
def wrapper(*a, **k): # takes the call
for _ in range(times):
result = fn(*a, **k)
return result
return wrapper
return decorator
Three levels, one per thing being received. Getting the count wrong is the standard mistake, and the symptom is a TypeError about a function not being callable, or a decorator that returns None.
The follow-up: what else can decorate
Anything callable. A class with __call__ is a common choice when the decorator needs state — a cache, a call count, a registry — because instance attributes are tidier than nonlocal. And the standard library's own decorators are worth naming: functools.lru_cache for memoisation, property, staticmethod, classmethod, contextlib.contextmanager and dataclasses.dataclass — which returns the same class with methods added rather than a wrapper, proving the return value need not be a function at all.
What to say out loud
The @ line is syntax for reassigning the name: greet = shout(greet). So the name ends up pointing at the wrapper, which is why __name__ and __doc__ change and tracebacks get less useful. functools.wraps copies the metadata over and sets __wrapped__ so introspection still works.
Then stop. The commonest failure on a question like this is answering it correctly in one sentence and then talking for another minute until something wrong comes out.
What to notice while it runs
- Decoration happens once, at definition time — not per call.
- Without
wraps, __name__ becomes wrapper and the docstring disappears.
__wrapped__ is the escape hatch back to the original.
Edge cases to raise
Volunteering these is most of what separates a correct answer from a good one.
A decorator can return anything. property returns a descriptor and dataclass returns the same class with methods added - so "a decorator wraps a function" is the common case, not the rule.
Decorating a method needs care about self. The wrapper receives it as the first positional argument, so *args handles it - but the ordering with staticmethod and classmethod matters, and those two go closest to the def.
Exceptions from the wrapper appear in the traceback. Every decorator adds a frame, which is why a heavily decorated call site produces tracebacks with more wrapper lines than application lines.
The follow-ups interviewers ask
"How do you write one that works with and without arguments?" Inspect the first argument: if it is callable and it is the only one, you were used bare and should decorate it immediately; otherwise return a decorator. It is fiddly, which is why most libraries simply require the parentheses.
"What should you know about functools.lru_cache?" It stores results on the function, keyed by the arguments - so the arguments must be hashable, and the cache keeps them alive. Decorating a method caches self, which keeps every instance alive for the life of the process: a real and common memory leak. maxsize=None makes it unbounded.
"In what order do stacked decorators apply?" Bottom-up. The one nearest the def receives the original function, and the one at the top receives whatever the others returned - which is why @app.route goes on top and @staticmethod goes on the bottom.
Common wrong answers
"The @ calls the function." It calls the decorator, once, with the function as its argument. Nothing calls the function until you do.
"functools.wraps makes it faster." It copies metadata. There is no performance claim to make, and the wrapper still costs one extra call per invocation.
"The decorator runs on every call." The decoration runs once, at definition. The wrapper runs on every call, and keeping those two separate is most of understanding decorators.
Recap in one screen
- Decoration happens once, at definition time - not per call.
- Without wraps, __name__ becomes wrapper and the docstring disappears.
- __wrapped__ is the escape hatch back to the original.
- The one-line answer: @shout above def greet is exactly greet = shout(greet).
- Worth trying: Remove the @functools.wraps line and call help(greet2). What disappears is what every documentation tool also loses.
- Worth trying: Stack @shout and @repeat(2) in both orders. The one nearest the def wraps first, and the outputs differ.