Home Session 06
Functions · Session 06

Decorators

A decorator looks like magic and is actually one line of assignment. You hand your function to another function, it hands back a replacement, and your old name gets pointed at the replacement. The original does not vanish — it is tucked inside, still doing the real work.

~40 minutes
5 interactive demos
Builds on Sessions 01 and 02
Learning goal

@deco means f = deco(f).

That is the entire feature. Session 01 taught that assignment moves a nametag; Session 02 taught that a function can remember an outer name. Put those two together and you have decorators. Everything else — arguments, stacking, the lost docstring — is a consequence of that one line.

function wrapper closure cell functools.wraps factory
Recommended path
1 Do it by hand first. Demo 1 has no @ at all — just an assignment.
2 Then add the @. Demo 2 ends in the same picture. Only the order is worth watching.
3 Ask where the original went. It is alive inside a closure cell, with no name pointing at it.
Interactive lab

Follow the nametag, not the @

Two things happen in every demo: a new function object appears, and an existing name is pointed somewhere new. If you can spot those two events, you can read any decorator you ever meet.

Tip: at the last step of each demo, ask “which object does the name point at, and what is keeping the original alive?”
1 Choose a demo
2 Step with or Play
3 Watch the name rebind
4 Read the model below
Names in the current frame
Function objects and closure cells
next back Space play R reset
python
1 / 1
Memory
stack heap
1

Nothing has run yet

Press Play or use Next to watch a decorator take a function apart.

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.

Model
A decorator is not a keyword
There is no decorator type in Python. Anything callable that accepts one argument works — a function, a class, an object with __call__. The @ is punctuation, not machinery.
Try this in a REPL

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.

Inside the wrapper

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.

greet
->
function wrapper @ 0x7f9101f0 closure cell -> function greet @ 0x7f9100c8
What moved

The name

greet was pointing at your function. Now it points at the wrapper. Nothing was copied or edited.

What stayed

The object

Your function still exists at the same address, with its refcount held up by the closure cell alone.

Key
This is Session 02's closure, doing a job
In Session 02 a closure remembered a counter. Here it remembers a function. Same mechanism, same diagram — only the thing being remembered has changed.
Identity

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
Care
Skipping wraps is a debugging tax you pay later
It costs one line and never changes behaviour. Without it, a stack trace three months from now points at wrapper in every decorated function in your codebase, and they all look the same.
Try this in a REPL

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.

Arguments

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.

Call 1 repeat(2) runs. Returns decorator, which remembers times = 2. decorator
Call 2 decorator(ping) runs. Returns wrapper, which remembers func and times. wrapper
Bind The name ping is pointed at wrapper. ping
Tell
Parentheses tell you which one you have
@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.
Stacking

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

What you should be able to draw

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.

Common mistakes

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.

Check
One question answers most decorator bugs
“After this line runs, what object does the name point at?” If the answer is 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.
Check your understanding

Can you predict the memory?

Every question is the same question: after this runs, what does the name point at?

1. What does @shout above def greet compile to?
A decorator never modifies your function. It calls something with it and rebinds the name to the result. If that result is not callable, the next call fails.
2. After decorating, what keeps the original undecorated function alive in memory?
No name points at it any more, but its reference count is not zero: the wrapper's closure cell still refers to it. That is the only reason calling the wrapper still works.
3. A decorated function reports __name__ == 'wrapper'. What fixes it?
wraps copies __name__, __doc__ and friends onto the wrapper and sets __wrapped__. Renaming the inner def would only work for one function; wraps works for all of them.
4. In @repeat(2), when does repeat(2) actually run?
repeat(2) is evaluated first and must return a decorator. That decorator is then called with your function. Two calls at import time, and your function body runs in neither.
5. With @bold written above @italic, which decorator receives your original function?
It compiles to text = bold(italic(text)). The inner call runs first, so italic gets your function and bold only ever sees italic's wrapper.
6. A decorator defines wrapper but its last line is missing. What happens?
A function with no return returns None, and f = deco(f) binds f to that. The failure shows up at the first call, not at the decorator.