1.3.8 · D1Python Intermediate

Foundations — Decorators — function decorators, @, wraps

1,706 words8 min readBack to topic

Before you can build a decorator you must own every small idea it silently rests on. The parent note uses words like callable, closure, first-class, rebind, *args, __name__, @. If any of those feel fuzzy, the whole topic collapses. So we build each one from zero, in the exact order they stack.


1 — A function is a thing, not just an action

Most beginners picture a function as a verb: "do this." But in Python a function is also a noun — an object you can hold in a variable, exactly like the number 5 or the string "hi".

def greet(): return "hi"
 
x = greet        # NO parentheses: x now IS the function
print(x)         # <function greet at 0x...>  — an object!
print(x())       # 'hi'  — parentheses = "run it now"

The single most important habit: greet is the thing; greet() runs the thing. Look at the two boxes below.

Figure — Decorators — function decorators, @, wraps
  • Left box: greet — a labelled box you can carry around.
  • Right box: greet() — you pressed the button, out pops the result "hi".

Related: First-class functions.


2 — "Callable": the thing you can put () after

Why introduce this word instead of just "function"? Because a decorator's real contract is "takes a callable, returns a callable" — slightly wider than "function", so the same idea works on classes and objects too.

Item
f = the callable itself; f() = its return value.

3 — Rebinding a name

A variable name in Python is a label stuck on a box. Assignment peels the label off one box and sticks it on another.

f = greet        # label 'f' points at the greet function
f = deco(f)      # peel 'f' off greet, stick it on deco's result
Figure — Decorators — function decorators, @, wraps

Follow the arrow: after f = deco(f), the name f no longer points at the original — it points at whatever deco returned. Nothing about the original code changed; only the label moved.


4 — Higher-order functions: functions that eat/return functions

A decorator is a specific shape of higher-order function: eats one function, returns one function. See Higher-order functions.

def apply_twice(fn):        # takes a function
    return fn(fn(0))        # calls it
 
def double(x): return x + 2
apply_twice(double)         # double(double(0)) = 4

5 — Nested functions and the closure (the memory trick)

You can def a function inside another function. The inner one can see the outer one's variables — and it keeps seeing them even after the outer function has finished. That kept-alive memory is a closure.

def make_adder(n):          # n lives here
    def adder(x):           # inner sees n
        return x + n        # n is a FREE variable of adder
    return adder            # hand back the inner, n still packed
 
add5 = make_adder(5)
add5(10)                    # 15  — it still remembers n = 5!
Figure — Decorators — function decorators, @, wraps

The figure shows adder walking away from make_adder carrying a backpack that still holds n = 5. That backpack is the closure.


6 — *args and **kwargs: the universal argument catcher

A wrapper must work for any function — one that takes 0 arguments, 5 arguments, keyword arguments, anything. Python gives two catch-all symbols.

def show(*args, **kwargs):
    print(args)     # a tuple of positional args
    print(kwargs)   # a dict of keyword args
 
show(1, 2, name="x")   # (1, 2)   and   {'name': 'x'}

The same stars used when calling do the reverse — they unpack:

func(*args, **kwargs)   # spread the tuple/dict back into arguments

7 — The @ symbol: sugar, not magic

@deco written above def f(): ... is pure shorthand for "define f, then rebind f = deco(f)." That's section 3's label-move, triggered automatically. It saves typing and puts the intent right at the top of the function.


8 — __name__ and __doc__: a function's name tag

Every function carries built-in labels — its identity card.

def add(a, b):
    "adds two numbers"
    return a + b
add.__name__   # 'add'
add.__doc__    # 'adds two numbers'

Prerequisite map

Functions are first-class objects

Callable: put parens to run

Rebinding a name f = deco of f

Higher-order functions

Nested functions

Closure remembers outer func

star args and star star kwargs

The at symbol is sugar

dunder name and doc labels

DECORATORS


Equipment checklist

Do I know the difference between f and f()?
f is the function object; f() runs it and gives the return value.
What does "first-class" mean for functions?
You can store, pass, and return them like any value.
What is a callable?
Anything you can put () after — functions, methods, __call__ objects.
What does f = deco(f) do to the name f?
Peels the label off the original and sticks it on deco's returned function.
What does @deco above def f desugar to?
f = deco(f).
What is a closure?
An inner function plus the outer variables it remembers (its free variables).
What is a free variable?
One a function uses but didn't define or receive as a parameter; it comes from an enclosing scope.
What does *args collect, and what does **kwargs collect?
*args → a tuple of positional args; **kwargs → a dict of keyword args.
What do * and ** do at a call site?
Unpack a tuple/dict back into positional/keyword arguments.
Why does the wrapper use *args, **kwargs?
So one wrapper forwards any arguments and works for every function signature.
What is func.__name__?
The function's name as a string — its identity label read by tools.
Why is functools.wraps needed?
Wrapping replaces the name tag with 'wrapper'; wraps copies the original's __name__/__doc__ back.

Connections