What does a decorator actually replace?

@shout above def greet is exactly greet = shout(greet). The name now refers to whatever the decorator returned — usually a wrapper function — so the original's name and docstring are gone unless you copy them across with functools.wraps.

Python semanticsConceptualMedium

Step through it

What to watch

  • 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.

Say this 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."

Overview

A decorator is a callable that receives a function and returns a replacement, often a wrapper. functools.wraps copies identifying metadata so tools and documentation still see the wrapped function’s name and docstring.

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.

1Python
Output

functools.wraps copies the metadata across, which is what makes the signature survive.

2Python
Output

A decorator taking an argument needs three layers - one per thing it receives.

3Python
Output

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.

Sources and next steps

How the code works

The metadata before and after, the wraps fix, and a decorator with an argument so the three layers are visible.

How the code works

  1. plain = shout(plain)The manual form, printed beside the decorated one to show they produce the same result. There is no extra machinery in the @.
  2. @functools.wraps(fn)A decorator applied to the wrapper. It copies the metadata from fn onto wrapper, which is why the name and docstring survive.
  3. inspect.signature(greet2)Reports the original signature because wraps set __wrapped__. Without it you get (*args, **kwargs), which is what makes wrapped APIs unpleasant to use.
  4. def repeat(times): def decorator(fn): def wrapper(...)Three layers because three things arrive separately: the argument, the function, and the call. Count them from the inside out.

Change one thing

  • Remove the @functools.wraps line and call help(greet2). What disappears is what every documentation tool also loses.
  • Stack @shout and @repeat(2) in both orders. The one nearest the def wraps first, and the outputs differ.

Where this runs

Real CPython, compiled to WebAssembly and running on your own machine — nothing is uploaded. The first run takes a few seconds while the interpreter downloads; after that it is immediate. Need more room, or want to paste your own attempt? Use the Python compiler.

Check yourself

0 of 4

Answer without scrolling back up.

  1. @shout above def greet is equivalent to:

  2. Why does __name__ become 'wrapper'?

  3. What does functools.wraps set that helps inspect.signature?

  4. Why does a decorator taking an argument need three nested functions?

Cheat sheet

What does a decorator actually replace?

See what the @ line does to a function object. Trace the replacement, add functools.wraps, and understand why the original name disappears without it.

INTERVIEW · vizlearn.in/interview/what-a-decorator-replaces.html

About the author

Ashish Jangra builds and maintains VizLearn. Every module here is written and the visualisation behind it hand-built, so the numbers in a readout come from the same code that draws the picture. Corrections are genuinely welcome and get priority over everything else — if a page states something wrong, or an animation misrepresents what the algorithm does, get in touch.