What the @ line actually means
In plain words
You wrote a function. Someone hands it to a helper and says “give me back a
better version.” The better version goes under the old name, and your
original goes in the helper's pocket. That is a decorator. The @
symbol is just a shorter way to write the handover.
Precisely: @deco above def f compiles to
f = deco(f). The def runs first and produces a
function object, that object is passed to deco, and the name
f is bound to whatever comes back.
The def runs
A function object appears on the heap. It has no name yet — the decorator line has not finished.
The decorator is called
Your function object goes in as an argument. Whatever comes out is usually a new function.
The name is assigned
The name in your module now points at the returned object. Session 01's rule, applied to a function.
decorator type in Python. Anything callable that
accepts one argument works — a function, a class, an object with
__call__. The @ is punctuation, not machinery.
Define def shout(f): ... and a plain def greet(name): ...,
then run greet = shout(greet) by hand. Compare id(greet)
before and after. You just decorated a function without typing @ once.
Where your original function goes
In plain words Nothing is thrown away. Your function is still there, holding the same address it always had. What changed is that no name points at it any more. It is reachable only through the wrapper that swallowed it.
The wrapper uses func, a name from the enclosing call. That makes
func a free variable, so Python attaches a closure cell
to the wrapper holding a reference to your function. The decorator's frame is
discarded when it returns; the cell survives because it belongs to the wrapper.
The name
greet was pointing at your function. Now it points at the wrapper. Nothing was copied or edited.
The object
Your function still exists at the same address, with its refcount held up by the closure cell alone.
The wrapper wears the wrong name tag
In plain words
You asked for a coat and got the coat back with someone else's name sewn in.
Your function still runs, but every tool that reads its label — help(),
a traceback, a log line — now reports the wrapper's name instead of yours.
functools.wraps(original) is a decorator you apply to the wrapper.
It copies __name__, __qualname__, __doc__,
__module__ and __annotations__ from the original onto
the wrapper, updates __dict__, and sets __wrapped__ to
the original object so tools can unwrap it.
| You call | Without wraps |
With wraps |
|---|---|---|
greet.__name__ |
'wrapper' |
'greet' |
greet.__doc__ |
None |
Your docstring |
greet.__wrapped__ |
AttributeError |
The undecorated function |
greet("ada") |
'HI ADA' |
'HI ADA' — behaviour is identical |
wraps is a debugging tax you pay laterwrapper in every decorated function in
your codebase, and they all look the same.
Decorate a function twice — once with @functools.wraps(func) on the
wrapper and once without — then compare help() on each. The difference
is what your future self will be reading.
Why @repeat(2) needs one more def
In plain words
The @ line only ever passes your function to one thing. If you also
want to pass a number, you need something that takes the number first and gives
back the thing that will take your function. That is the third layer.
@repeat(2) evaluates repeat(2) immediately — before any
decorating happens — and the result is what gets applied to your function.
repeat is therefore a decorator factory, not a decorator.
Two calls happen at import time; your function body still runs zero times.
repeat(2) runs. Returns decorator, which remembers times = 2.
decorator
decorator(ping) runs. Returns wrapper, which remembers func and times.
wrapper
ping is pointed at wrapper.
ping
@deco means deco receives your function.
@deco() means deco() is called first and its result
receives your function. Mixing these up is the single most common decorator bug —
and the error message is usually a baffling TypeError about the wrong
number of arguments.
Two decorators, two different orders
In plain words Think of wrapping a parcel. The layer you put on first ends up innermost; the last layer you add is the one someone opens first. Decorators are applied from the bottom up and entered from the top down, for exactly that reason.
@bold above @italic above def text compiles to
text = bold(italic(text)). The inner call happens first, so
italic is applied first and ends up nested inside bold's
wrapper. Calling text() then enters bold's wrapper first.
Applied bottom-up
italic(text) runs, then bold(...) runs on its result.
The decorator nearest the def gets your real function.
Executed top-down
text() enters bold's wrapper, which calls italic's wrapper,
which calls your function. Result: <b><i>hi</i></b>.
A name, an outermost wrapper, a closure cell inside it holding the next wrapper, and your original function at the bottom of the chain. If you can draw that, you can predict what any stack of decorators does — and where to look when one of them breaks.
Four ways this goes wrong
Forgetting to return the wrapper
A decorator that defines wrapper but never returns it returns
None. Your name is then bound to None, and the first
call gives TypeError: 'NoneType' object is not callable. If you see
that error right after adding a decorator, check its last line.
A wrapper that only accepts the arguments you tested with
def wrapper(text) works until someone decorates a two-argument
function. Write def wrapper(*args, **kwargs) and pass them through
with func(*args, **kwargs) unless you have a reason not to.
Leaving out functools.wraps
Behaviour is fine; everything that reads a function's labels is not.
Tracebacks, help(), API docs and log lines all start reporting
wrapper. One line fixes it permanently.
Writing @deco() when the decorator takes no arguments
That calls deco with nothing, then applies whatever it returned
to your function. If deco expected a function, you get a
TypeError at import time — before any of your code runs.
The reverse, @repeat without (2), silently binds your
function to the wrong layer.
None, you forgot a return. If it is
your original function, the decorator did not wrap. If it is a wrapper with the
wrong signature, the *args are missing.