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.

Overview

The question, and what it is testing

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

Interviewers use this one because the answer separates people who have read the language from people who have only used it. Nothing here is obscure; all of it is observable, which is what the editor below is for.

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

What does a decorator actually replace?

What does the @ syntax do, and why do decorated functions need functools.wraps?

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

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?

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

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.