Decorators — function decorators, @, wraps
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 replacementWhy 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 wrapperWhy return result? If we forget it, the wrapped function silently returns None — a classic bug.

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 wrapperWorked 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?
wrapper) that replaces the original function.Why use *args, **kwargs in the wrapper?
What does functools.wraps do?
__name__, __doc__, etc.) from the original onto the wrapper so introspection still works.Why does forgetting return func(...) break things?
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?
func variable (a closure).Connections
- Closures and free variables
- First-class functions
- functools — wraps, lru_cache, partial
- Higher-order functions
- Class decorators and property
- args and kwargs unpacking
Concept Map
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!