Decorators, unwrapped

Closures & late binding showed a nested function reaching into its enclosing scope. A decorator is the single most common thing built that way: @deco above a def is nothing more than f = deco(f), run once, right after the function is defined, and the closure it creates is what remembers the original f every time the wrapper is called. Everything below — arguments, stacking, lru_cache — is that one idea, layered.

f = deco(f)functools.wraps stacking orderlru_cache

A call travels inward, a return travels outward

Each ring below is one decorator's wrapper, wrapped around the layer inside it; the original function sits at the centre. Calling the decorated name calls the OUTERMOST ring first — the call travels inward, layer by layer, until it reaches the real function, and the return value travels back outward through every layer it passed through, in reverse.

A decorator is f = deco(f), not magic

@deco written above a def desugars to exactly one thing: define the function, then immediately call f = deco(f) and rebind the name to whatever deco returns. Nothing runs later that isn't already implied by that line — the "magic" is entirely in deco choosing to return a different, wrapping function instead of f itself.

Why functools.wraps is not cosmetic

A wrapper function is a perfectly ordinary function with its own __name__ ("wrapper"), its own __doc__ (None, unless you wrote one), and no idea it's standing in for something else. Anything that introspects a function to do its job — help(), API doc generators, logging that prints fn.__name__, pickle trying to find the function by its qualified name — now sees the wrapper's identity instead of the real one. functools.wraps(f) copies __name__, __doc__, __module__ and more from f onto the wrapper, and also sets wrapper.__wrapped__ = f so tooling can still find the original underneath.

Decorators with arguments are one ring further out

@repeat(3) is not itself a decorator — repeat(3) is a plain function call that returns one. repeat is a factory: it takes the arguments, and returns the actual decorator, which then takes f and returns the wrapper. Three calls, three functions, one extra ring around everything you've already seen.

Stacking order: applied last, runs first

@A above @B above def f applies B first — f = A(B(f)) — which makes A's wrapper the OUTERMOST ring. Read bottom-up for application order, top-down for execution order: A's wrapper is what actually runs first when you call the decorated name, and its return is the last thing to happen.

Takeaways: @deco means f = deco(f), run once. A wrapper is a real function with its own identity — functools.wraps is what makes it keep pretending to be the original for every tool that asks. A decorator "with arguments" is a factory: one more ring, built by a plain function call before the real decoration happens. Stacked decorators apply bottom-up but the outermost one runs first and returns last. functools.lru_cache is a decorator like any other — a ring that remembers past calls instead of re-running the one inside it.