skip to content

Decorator Mechanics

A decorator is just a callable that takes a function and returns a replacement, so factories, stacking and lost metadata all follow from that rule. Interviewers use @ to test first-class functions.

part ofPythonoverview, primer and where to startread it →
on this pageshow

questions

page 1 of 2

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

open as a page

What do the maxsize and typed arguments to functools.lru_cache control?

level: juniorimportance: must knowfreq 50%

basics

~20 s

maxsize caps how many results are stored - 128 by default, None for unbounded, 0 for none - and a full table drops the least recently used entry. typed=True adds argument types to the key, so 1 and 1.0 stop sharing.

open as a page

What object does a Python class decorator receive, and what does the class name end up bound to?

level: juniorimportance: must knowfreq 45%

basics

~20 s

A class decorator is called with the finished class object, after the class body has already executed, and whatever it returns is bound to the class name. Most decorators mutate the class and return the same object.

open as a page

Why must a decorator wrapping an `async def` function define its wrapper with `async def` and await inside?

level: juniorimportance: must knowfreq 60%

basics

~20 s

Calling an async def function runs none of its body; it returns a coroutine object. A plain def wrapper only holds that unstarted coroutine, so anything it does around the call observes nothing. The wrapper must be async def and await inside.

open as a page

Why does @retry(times=3) need one more function layer than a bare @retry?

level: juniorimportance: must knowfreq 60%

basics

~20 s

@retry(times=3) calls retry(times=3) first and decorates with whatever that call returns. So retry must be a factory that returns a decorator, which then takes the function and returns the wrapper - three nested levels instead of two.

open as a page

How does `self` reach the wrapper of a decorated instance method?

level: juniorimportance: must knowfreq 50%

basics

~20 s

Decoration runs in the class body, on the plain function, before any binding exists. The class attribute is now the wrapper, so attribute lookup binds the wrapper, and the instance arrives as the wrapper's first positional argument inside *args.

open as a page

What does the @functools.singledispatch decorator do to the function it decorates?

level: juniorimportance: must knowfreq 32%

basics

~20 s

It turns the function into a generic function: the decorated body becomes the fallback implementation, and its register() attribute lets you attach one implementation per argument type. Each call picks an implementation from the type of the first positional argument.

open as a page

When `@a` is stacked above `@b` on a Python function, which decorator wraps the function first and which one runs first at call time?

level: juniorimportance: must knowfreq 68%

basics

~10 s

Decorators apply bottom-up: @b wraps the function first, then @a wraps that result, exactly like writing f = a(b(f)). At call time the outermost wrapper runs first, so @a's code executes before @b's.

open as a page

What does functools.wraps fix about a decorator's wrapper function?

level: juniorimportance: must knowfreq 72%

basics

~20 s

functools.wraps copies the decorated function's identity onto the wrapper - its name, qualname, doc, module and the contents of dict - so help(), registries and documentation tooling describe the original function instead of an anonymous wrapper.

open as a page

Why does a Python decorator's wrapper need *args, **kwargs and a return?

level: middleimportance: must knowfreq 70%

basics

~20 s

Because the wrapper replaces the function it decorates, it is the callable everyone now invokes. Accepting *args and **kwargs lets it take any call shape, forwarding them calls the original correctly, and returning that call's value keeps the result from becoming None.

open as a page

Why does an @functools.lru_cache-decorated function raise TypeError when passed a list?

level: middleimportance: must knowfreq 52%

basics

~20 s

The cache key is built from the call's arguments and used in a dictionary, so every argument must be hashable. A list is unhashable, so building the key raises TypeError before the wrapped function ever runs. Pass a tuple or a frozenset instead.

open as a page

How does a class implementing __init__ and __call__ work as a Python decorator?

level: middleimportance: must knowfreq 55%

basics

~20 s

@Cls above a def calls Cls(func), so __init__ receives the function once at decoration time and stores it plus any state. The decorated name is now an instance; calling it runs __call__, which invokes the stored function.

open as a page

How do you make a Python decorator remember state, such as a call count, between calls?

level: juniorimportance: should knowfreq 50%

basics

~20 s

Put the state in the decorator's own scope, outside the wrapper: a variable rebound with nonlocal, a mutable object such as a dict, or an attribute set on the wrapper function. The decorator body runs once, so that state survives every call.

open as a page

How does @functools.cached_property differ from @property on repeated attribute access?

level: middleimportance: should knowfreq 46%

basics

~20 s

property runs its function on every attribute read. functools.cached_property runs it on the first read only, stores the result in that instance's dict under the same name, and every later read finds that ordinary instance attribute, so the function is never called again.

open as a page

What breaks when a Python class decorator returns a wrapper function instead of the class?

level: middleimportance: should knowfreq 30%

basics

~20 s

The decorated name is rebound to whatever the decorator returns. Return a function or a proxy instance and that name is no longer a class: isinstance and issubclass raise TypeError, nothing can subclass it, and pickle cannot find the type.

open as a page

How do you write one decorator that wraps both plain functions and `async def` functions?

level: middleimportance: should knowfreq 45%

basics

~10 s

Branch once at decoration time on inspect.iscoroutinefunction(func) and return one of two wrappers: an async def one that awaits the call, or a plain def one that does not. Apply functools.wraps in both branches.

open as a page

How do you make one decorator work both as @retry and as @retry(times=3)?

level: middleimportance: should knowfreq 40%

basics

~10 s

Give it a first parameter defaulting to None and keyword-only options: def retry(func=None, *, times=3). When func is None the decorator was called with options, so return functools.partial(retry, times=times); otherwise wrap func directly.

open as a page

Why does a class-based decorator break when applied to an instance method?

level: middleimportance: should knowfreq 30%

basics

~20 s

The class attribute is now an instance of your decorator class, and ordinary instances are not descriptors: they have no __get__. Attribute lookup returns the object as-is, nothing prepends the instance, and the wrapped function loses its first argument.

open as a page

Why must `@staticmethod` sit outermost when stacked with your own decorator?

level: middleimportance: should knowfreq 45%

basics

~20 s

Decorators apply bottom-up, and staticmethod returns a descriptor object rather than a function. Put it outermost so it wraps your wrapper; put it underneath and your wrapper becomes an ordinary class attribute that binds as an instance method.

open as a page

Why does a functools.singledispatch handler registered for int also receive True?

level: middleimportance: should knowfreq 24%

basics

~10 s

Because bool is a real subclass of int. Resolution walks the argument's class hierarchy and picks the most derived registered class, so booleans land on the int implementation unless you register bool explicitly.

open as a page

Why does @functools.singledispatch always run the fallback when applied to a method?

level: middleimportance: should knowfreq 26%

basics

~20 s

Dispatch reads the first positional argument, which on an instance method is self. Its type never varies, so every call resolves to the same implementation. Use functools.singledispatchmethod, which dispatches on the first argument after self.

open as a page

When you stack a permission-check decorator and a caching decorator on one Python handler, which one must be on top?

level: middleimportance: should knowfreq 42%

basics

~20 s

The permission check goes on top, outermost. A caching decorator returns early on a hit, so anything beneath it is skipped for that call — with the cache outermost, a cached result is served without the check ever running.

open as a page

How does inspect.signature see past a @functools.wraps wrapper to the original parameters?

level: middleimportance: should knowfreq 46%

basics

~10 s

functools.wraps sets wrapper.wrapped to the original callable, and inspect.signature follows that link by default. Pass follow_wrapped=False to see the wrapper's real (*args, **kwargs) parameters, or walk the chain yourself with inspect.unwrap.

open as a page

At what point does a Python decorator's own body run relative to calls?

level: seniorimportance: should knowfreq 45%

basics

~20 s

The decorator body runs once, when the def it sits above is executed — normally while the module is first imported. Only the wrapper it returns runs per call, so anything done in the decorator body is import-time work, not per-call work.

open as a page

Why does an @functools.lru_cache-wrapped formatter in an invoice-PDF renderer keep returning the previous currency format after a global setting changes, and how do you fix it?

level: seniorimportance: should knowfreq 40%

basics

~20 s

The cache key is built from the arguments only. The currency setting is ambient state the key never sees, so a call with the same amount hits the entry stored before the change. Fix it by making the setting an explicit parameter, or clear the cache when it changes.

open as a page

Why can a class-decorator registry leak memory in a long-running service, and how do you bound it?

level: seniorimportance: should knowfreq 30%

basics

~20 s

The registry dict holds a strong reference to every class it records. That is harmless when classes are defined once at import, but a service that builds classes at runtime pins each one, plus everything reachable from it, forever. Bound it by not generating classes per request, or by using a weak-valued mapping.

open as a page

Why does `inspect.iscoroutinefunction` return False for a decorated async function, and how do you fix the wrapper?

level: seniorimportance: should knowfreq 30%

basics

~20 s

The name now points at the wrapper, and a plain def wrapper is a synchronous callable no matter what it returns. functools.wraps copies metadata but not kind. Either make the wrapper async def and await inside, or call inspect.markcoroutinefunction on it.

open as a page

Why does a decorator factory called as @valid_until(time.time() + 3600) freeze one deadline?

level: seniorimportance: should knowfreq 30%

basics

~20 s

Because the argument expression runs once, when the decoration executes at import, not on each call. The factory captures that single timestamp in its closure, so every later call compares against the same fixed instant rather than a rolling hour.

open as a page

Why does `functools.lru_cache` on a method keep its instances alive?

level: seniorimportance: should knowfreq 35%

basics

~20 s

The decorator is applied once, to the plain function, so one cache is shared by the whole class. The instance arrives as part of the cache key, and the cache holds it by a strong reference — so every instance ever scored stays reachable.

open as a page

When is functools.singledispatch a better fit than an isinstance ladder or a method on the class?

level: seniorimportance: should knowfreq 30%

basics

~20 s

Prefer it when the operation does not belong on the types, or those types are not yours to edit, so new cases arrive as registrations. Keep a method when you own the hierarchy, a ladder when branching is small and closed.

open as a page

showing 1–30 of 34