1.3.8Python Intermediate

Decorators — function decorators, @, wraps

1,699 words8 min readdifficulty · medium4 backlinks

WHY do decorators exist?

You constantly want to add behaviour around existing functions without editing their bodies: timing, logging, caching, access-checks, retries.

The naïve fix is to paste the same start = time(); ...; print(elapsed) lines into every function. That violates DRY and means 50 edits if you change the logging format.


WHAT is a decorator, precisely?

So these two are identical:

@deco
def f(): ...
def f(): ...
f = deco(f)

HOW to build one from scratch (derivation)

Goal: a timer that prints how long any function takes.

Step 1 — what must timer accept and return? By the definition it must accept the original function func and return a replacement function. Why? Because after @timer, the name now points to whatever timer returns.

def timer(func):          # takes the function
    def wrapper():        # the replacement
        ...
        return func()     # still calls the original
    return wrapper        # hand back the replacement

Why an inner wrapper? We need a place to run extra code around func(). The inner function "remembers" func via a closure.

Step 2 — make it forward any arguments. The original might take args. The wrapper must pass them through, so use *args, **kwargs. Why? So one decorator works for every signature.

def timer(func):
    def wrapper(*args, **kwargs):
        import time
        start = time.perf_counter()
        result = func(*args, **kwargs)   # capture & forward the return value
        print(f"{func.__name__} took {time.perf_counter()-start:.4f}s")
        return result                    # WHY: must return original's value
    return wrapper

Why return result? If we forget it, the wrapped function silently returns None — a classic bug.

Figure — Decorators — function decorators, @, wraps

WHY functools.wraps?

After wrapping, f.__name__ becomes "wrapper" and f.__doc__ is lost — because the name now points at the inner function. This breaks help(), debuggers, and introspection.

from functools import wraps
 
def timer(func):
    @wraps(func)                 # <-- copies func's identity onto wrapper
    def wrapper(*args, **kwargs):
        ...
    return wrapper

Worked examples


Common mistakes


Forecast-then-Verify

Recall Predict the output, then check
def deco(f):
    def w(*a, **k):
        return f(*a, **k) * 2
    return w
@deco
def g(x): return x + 1
print(g(4))

Forecast: g(4) → original gives 5, doubled → 10. (Desugar: g = deco(g).)


Recall Feynman: explain to a 12-year-old

Imagine you have a toy robot that says a number. A decorator is like putting it inside a fancy box. The box still lets the robot say its number, but the box also claps every time, or repeats it twice. You didn't change the robot — you wrapped it. The @ is just a sticker on the box that says "use this wrapper." And wraps is keeping the robot's name label on the outside of the box so you still know which robot is inside.


Flashcards

What does @deco above def f desugar to?
f = deco(f) — rebinds the name to deco's return value.
What must a decorator return?
A callable (usually an inner wrapper) that replaces the original function.
Why use *args, **kwargs in the wrapper?
So the wrapper forwards any arguments, working for every function signature.
What does functools.wraps do?
Copies metadata (__name__, __doc__, etc.) from the original onto the wrapper so introspection still works.
Why does forgetting return func(...) break things?
The wrapper returns None, so the wrapped function silently loses its return value.
How does @repeat(3) desugar?
f = repeat(3)(f) — the factory is called first, returning the actual decorator.
How does stacked @a over @b over def f evaluate?
f = a(b(f)) — bottom (nearest def) applied first.
What enables closures to "remember" func?
The inner wrapper closes over the enclosing func variable (a closure).

Connections

Concept Map

violates

solved by

enables

desugars to

defines

built with

uses

must forward

must

loses metadata

applied to

restores

First-class functions

Need to add behaviour without editing bodies

Avoid repeated code / DRY

Decorator: callable taking callable returns callable

@deco sugar

f = deco of f

Inner wrapper function

Closure remembers func

Forward args, kwargs

Return original result

functools.wraps

Preserves __name__ and __doc__

Hinglish (regional understanding)

Intuition Hinglish mein samjho

Dekho, decorator ka funda simple hai: ye ek function hota hai jo doosre function ko leta hai aur ek naya function wapas deta hai. Python me functions "first-class" hote hain — matlab tum unhe variable ki tarah pass aur return kar sakte ho. Isi power ka use karke hum kisi function ke around extra kaam (timing, logging, caching) add karte hain, bina uska original code chhede.

Wo jo @deco likhte ho def f ke upar, wo sirf shortcut hai. Asli me Python karta hai f = deco(f). Bas itna yaad rakho — @ ka matlab "assign back". Andar ek wrapper function banate hain jo *args, **kwargs use karta hai taaki kisi bhi function ke saath kaam kare, aur return func(*args, **kwargs) zaroor karna warna original ka return value gum ho jaata hai (ek bahut common bug!).

functools.wraps ka kaam hai original function ka naam aur doc copy karna wrapper pe. Agar ye na lagao to f.__name__ ban jaata hai "wrapper", aur debugging/help() me confusion hota hai. Toh hamesha @wraps(func) lagao — ye best practice hai.

Parametrised decorator jaise @repeat(3) me teen layer lagti hain: pehli layer n pakadti hai, doosri func pakadti hai, teesri actual kaam karti hai. Ye desugar hota hai f = repeat(3)(f). Stacking me bottom-to-top apply hota hai: @a @b def f matlab f = a(b(f)). Bas yahi core hai — Take, Wrap, Return!

Go deeper — visual, from zero

Test yourself — Python Intermediate

Connections