Programming with AI
ENRU
2.9

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:

measured
slow_sum: 0.011 s
499999500000

The 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

DecoratorWhat it does
@router.message(...) (aiogram)"call this function on such a message"
@app.get("/path") (FastAPI)"call it on such an HTTP request"
@dataclasswrites __init__ and the printout for you
@propertya method read like a field, without parentheses
@staticmethoda method that does not need self
@functools.cacheremembers 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 wrappers

In React, hooks play a similar role: you never call useEffect directly, React calls it itself at the right moment.

What to tell the AI

Prompt
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:

Prompt
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

  • @name above 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.get register a function: the framework calls it.

  • A function never called yet working — look for the decorator above it.

  • Several decorators apply bottom-up.