skip to content

What does the @ decorator syntax above a Python def actually do?

level: juniorimportance: must knowfreq 85%

answer

  1. Sugar over an ordinary call
  2. Two steps: build, then rebind
  3. The def name is reassigned
  4. f = deco(f)

basics

~20 s

@deco written above def f() is shorthand for defining f and then rebinding that name: f = deco(f). A decorator is any callable that takes the function object and returns the object the name will point at.

solid answer

~40 s

`@deco` is syntax sugar, not a runtime feature. Python executes the `def` to build the function object, calls `deco` with that object, and binds the name to whatever `deco` returns — exactly `f = deco(f)`. So a decorator is any callable of one argument, and nothing forces it to return a function: a decorator that forgets its `return` leaves the name bound to `None`, and the next call raises `TypeError`. The common shape returns a closure — an inner `wrapper` that captures `func`, does something before or after, and calls it — which means callers now reach the wrapper, and the original function survives only inside that closure. Since Python 3.9 the expression after `@` may be any expression, not just a dotted name with an optional call.

code

python · 15 lines
python
def announce(func):
    def wrapper(*args, **kwargs):
        print("calling", func.__name__)
        return func(*args, **kwargs)
    return wrapper

@announce
def total(a, b):
    return a + b

def total_manual(a, b):
    return a + b
total_manual = announce(total_manual)

print(total(2, 3), total_manual(2, 3))

go deeper

for a junior

Be ready to write the two-line equivalent, f = deco(f), on the whiteboard without hesitating, and to say that a decorator is just a callable taking the function object and returning its replacement.

for a middle

Explain the mechanics: the def executes first, the decorator is called once with the resulting object, and the name is bound to the return value — so a missing return leaves the name as None.

for a senior

Show what the rebinding costs in production: callers, introspection and identity checks all see the wrapper, so the wrapper is the real public callable and must behave like the thing it replaced.

for a principal

Own the judgement of when a decorator is the right seam at all, versus an explicit call, a context manager or configuration — decoration hides an indirection in every call site that reads it.

### The desugaring, stated once A decorator is not a special kind of object, and `@` is not a runtime mechanism. `@deco` on the line above a `def` is grammar that expands into two ordinary steps. Python first executes the `def` statement and builds a function object exactly as it always does. Then, instead of binding that object to the name, it calls `deco` with it and binds the **result** to the name in the enclosing namespace. So this: ```python @deco def f(x): return x ``` is the same program as this: ```python def f(x): return x f = deco(f) ``` with one small difference: in the sugared form the undecorated function is never bound to the name at all, so no other code can observe it under that name, not even briefly. Every other decorator behaviour you will ever be asked about follows from that single rebinding. ### What qualifies as a decorator Anything callable that accepts one positional argument. A plain function is the usual case, but a class whose instances are callable works too, and so does a builtin. The interpreter does not inspect or validate the return value. A decorator may return the same object it was handed — the registration idiom, where the point is the side effect and the function is passed through untouched — or a brand-new function, or an instance of some class, or `None`. Returning `None` is the classic beginner's bug: the `def` seems to vanish, the name is bound to `None`, and the failure surfaces far from its cause as `TypeError: 'NoneType' object is not callable`. Since Python 3.9 (PEP 614) the expression after `@` may be any expression — a subscript, a call whose result is itself called, a conditional expression. Older grammars allowed only a dotted name with an optional argument list. On 3.14 the relaxed grammar is simply the rule; you rarely need it, but it explains why `@things[0]` parses. ### The wrapper-closure shape Most useful decorators need to run code around each call, and the only way to do that is to hand back a different callable. That callable is normally a nested function that closes over the original: ```python def announce(func): def wrapper(*args, **kwargs): print("calling", func.__name__) return func(*args, **kwargs) return wrapper ``` `wrapper` is created fresh each time `announce` runs, and it captures `func` in its closure. After `@announce` is applied, the module-level name refers to `wrapper`; the original function object is still alive, but the only reference to it is the closure cell inside that wrapper. This is why a decorated function is not "modified" — nothing about the original object changes. It is wrapped, and the *name* is what moved. ### Everything the name change implies Because callers reach the wrapper, the wrapper is now the public callable. Its parameters are the signature callers must satisfy, its return value is what callers receive, and its identity is what introspection sees: `f.__name__` reports `'wrapper'` unless the decorator copies the metadata across, which `functools.wraps` exists to do and which is its own topic. Comparisons by identity, pickling by name, and anything else keyed on the object all now see the wrapper. None of that is surprising once you hold on to `f = deco(f)`. ### Applying a decorator by hand Since `@` only rebinds a name, you can always do it yourself: ```python def build_pick_list(order): return sorted(order) build_pick_list = announce(build_pick_list) ``` That is the escape hatch when the target is not yours to annotate — a callable imported from elsewhere, a function you want to decorate only under a runtime condition, or a lambda stored in a table. It is also the shape to recall in an interview when someone asks what `@` "compiles to": there is no separate mechanism to describe, only a call and a binding. ### The mental model to keep Read `@deco` as "hand this new function to `deco` and use whatever comes back". A junior who can state that sentence, and who knows that the decorator body runs at definition time while the wrapper runs per call, already has the whole model; decorators with arguments, stacked decorators, metadata preservation and class decoration are all variations layered on top of one rebinding.

  • Must a decorator return a function?
    No. The interpreter binds the name to whatever the decorator returns, with no check at all. Returning the original function unchanged is a legitimate registration idiom; returning a callable instance is common for stateful decorators; returning `None` is a bug that shows up later as `TypeError: 'NoneType' object is not callable` at the first call.
  • How would you decorate a function you cannot put an @ line above?
    Do the rebinding by hand: `target = deco(target)`. That works for a callable imported from another module, for applying a decorator only when some runtime condition holds, and for wrapping a callable held in a list or dict. The `@` form has no capability the manual call lacks.
  • After decorating, can you still reach the original undecorated function?
    Not through the name — that now points at whatever the decorator returned. The original object is usually still alive inside the wrapper's closure, and well-behaved wrappers additionally expose it as an attribute so tests can reach it. If the decorator kept no reference at all, the original is unreachable and eligible for collection.

Signing for a parcel and handing on a different box: the delivery still happens under the same address label, but what the recipient opens is whatever the signer decided to pass along.

saying these in an interview costs you the question

  • Says @ modifies the original function in place
  • Thinks the decorator is called on every call
  • Believes a decorator must return a function
  • Cannot write the f = deco(f) equivalent
  • Assumes @ is a compiler feature with no call involved

context