Functions are values
A decorator is a function that takes a function and returns a new one. Line 1 defines ours, shout. Like any def, it builds a function object and stores it under its name. Its body waits.
Line 5, @shout, marks the next def as decorated. Line 6 builds the function greet, an object like any other value. Because of the @, the name greet is not stored yet: the function goes to shout first.
What @ does
Now Python calls shout with greet as its argument. The function itself travels in, like a number would; inside shout the parameter func names it. Values you can pass and return like this are called first-class.
shout's body runs. Line 2 builds a new function, wrapper, inside shout. wrapper uses func, so Python keeps func in a cell attached to wrapper, even after shout ends. A function that carries names from around it is a closure.
Line 4 returns wrapper, and Python stores it under the name greet. So @shout above def greet means greet = shout(greet). The name greet now points to wrapper; the original is reachable only through func.
Calling greet
Line 8 calls greet("sam"). Python looks up greet and finds wrapper, so wrapper runs with name = 'sam'. The caller never sees the swap: a decorator adds behaviour without editing greet or its callers.
Line 3 calls func(name). func is the original greet, kept in wrapper's closure, so the original function finally runs, with name = 'sam'. wrapper waits in the middle of line 3 for the answer.
Line 7, the original body, returns "hi " + name: the string 'hi sam'. The value goes back to wrapper, the function that called it.
wrapper finishes line 3: .upper() turns 'hi sam' into 'HI SAM', and it returns that. Back on line 8, print() shows HI SAM. The decorator changed the answer without touching greet's code.
Good habits
One catch: greet.__name__ now says 'wrapper', which muddles error messages and help(). Put @functools.wraps(func) on the line above def wrapper: it copies func's name and docstring onto wrapper. Use it in every decorator.
Try it yourself
;; step 1/10 · line 1: def shout(func): >>> def shout(func): ... def wrapper(name): ... return func(name).upper() ... return wrapper ... >>> shout # nothing inside has run yet <function shout at 0x…>
The order is exact: Python evaluates the decorator expression (shout), builds the function from def, calls the decorator with it, and only then binds the name. The reference says @shout def greet is roughly greet = shout(greet), except that the original is never bound to the name greet, even for a moment. The figure writes a function object as fn and its __name__. Python shows it as <function greet at 0x…>; the number is a memory address that changes from run to run. Without wraps, Python shows the wrapper as <function shout.<locals>.wrapper at 0x…>.