Decorators and “magic”
why code does not execute top to bottom
What it is
A decorator is a line with an @ above a function. It means: this function is
wrapped in another one, and on a call the wrapper runs first.
You need to know this for one simple reason: because of decorators, code does not execute in the order it is written. For anyone reading code, that is the main source of bewilderment.
What it looks like in code
import functools
import time
def timed(fn):
@functools.wraps(fn)
def wrapper(*args, **kwargs):
t = time.perf_counter()
result = fn(*args, **kwargs)
print(f"{fn.__name__}: {time.perf_counter() - t:.3f} s")
return result
return wrapper
@timed
def slow_sum(n: int) -> int:
return sum(range(n))
print(slow_sum(1_000_000))Output:
slow_sum: 0.011 s
499999500000The timing message printed before the result, although in the code the
print comes after. That is how the wrapper works: the call slow_sum(...)
actually calls wrapper, which runs the real function, measures the time,
prints, and only then returns the result.
How to read it
@name above a function — the function has been wrapped. To know what
happens on a call you have to look at name, not only at the function itself.
*args, **kwargs means "any arguments at all". The wrapper does not know
what the original function takes, so it takes everything and passes it on.
functools.wraps is housekeeping: it keeps the original function's name
and docstring on the wrapper. Without it, slow_sum.__name__ would return
wrapper and tracebacks would stop making sense.
The decorators you will meet most
| Decorator | What it does |
|---|---|
@router.message(...) (aiogram) | "call this function on such a message" |
@app.get("/path") (FastAPI) | "call it on such an HTTP request" |
@dataclass | writes __init__ and the printout for you |
@property | a method read like a field, without parentheses |
@staticmethod | a method that does not need self |
@functools.cache | remembers the result; same argument, no recomputation |
The first two matter most in practice. They register the function: you never call it anywhere, yet it gets called when the right event arrives.
@router.message(Command("weather"))
async def cmd_weather(message: Message) -> None:
...Searching the project for a call to cmd_weather finds nothing. That is
normal: the framework calls it, and the @ line is the application for that.
Other languages
The same thing exists everywhere, under other names:
// JavaScript: the wrapper is explicit, no @ syntax
const timedSum = withTiming(slowSum);// Dart: annotations @override, @immutable — more often hints
// than wrappersIn React, hooks play a similar role: you never call useEffect directly,
React calls it itself at the right moment.
What to tell the AI
What does the @timed decorator do to slow_sum?
Explain the order in which the lines execute on a call.When it isn't clear who calls a function:
This function is never called in the code. Who calls it
and when?What to look for
The order of decorators. If there are several, they apply bottom-up — the one closest to the function first. Swapping them changes the behaviour.
A decorator silently swallowing errors. A wrapper with a try/except
inside means errors from the real function may never reach you (article 2.8).
A wrapper that changes the arguments or the result. Read what exactly is
passed into fn(...) and what comes back: a wrapper can substitute both.
No functools.wraps. A small thing, but because of it tracebacks show
wrapper instead of function names, and debugging gets harder.
Cheat sheet
@nameabove a function — it is wrapped; look at the wrapper.Because of decorators, code does not execute top to bottom.
*args, **kwargs— "I take anything and pass it on".@router.message,@app.getregister a function: the framework calls it.A function never called yet working — look for the decorator above it.
Several decorators apply bottom-up.