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 at once. It runs once, when the def is executed, not on each 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 means that function. And it has its own __name__, its own empty __doc__ and its own signature. Code that reads these attributes can break 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 first. 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. For example, a callable object can keep a cache or call count in instance attributes instead of using 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.
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. Each decorator adds a frame. This is why a heavily decorated call site gives tracebacks with more wrapper lines than use lines.