Decorators
A decorator in Python is not a special syntactic construct with its own semantics — it is an ordinary call to an ordinary function, written more briefly. The line @dec above def f expands to exactly f = dec(f): the interpreter builds the function, passes the function object to dec, and binds the name f to whatever dec returned. Every decorator behaviour follows from that single substitution, and almost every interview mistake comes from never having said it out loud.
Three non-obvious facts follow immediately. First, a decorator can be any callable — a function, a lambda, a class, an instance with __call__ — and the target can be a function, a method, or a class. Second, the returned wrapper is a different object, so __name__, __doc__ and __qualname__ get replaced, and introspection breaks without functools.wraps. Third, a decorator runs at definition time, not at call time — and when a decorator needs an argument, a third level of nesting is required, because @foo() calls foo first and only then applies the result to the function. Work through each mechanism in the layers below.
Topic map
- The decorator and the @ sugar —
@decabovedef fis exactlyf = dec(f); everything else follows from it. - Any callable — a function, a
lambda, a class or an instance with__call__can decorate; the target can be a function, a method or a class. - functools.wraps and metadata — the wrapper replaces
__name__,__doc__,__qualname__;wrapscopies them back and sets__wrapped__. - The decorator factory —
@foo()callsfoofirst, so a parametrized decorator needs three levels of nesting. - Stacking order — a decorator stack is applied bottom-up but entered top-down at call time.
- @repeat — repetition and timing — an applied factory that ties together the three levels,
wraps,*args/**kwargsandtime.perf_counter.
Common mistakes and traps
| Mistake | Consequence |
|---|---|
Thinking @dec calls f and caches its result | The decorator runs once at definition time and returns an object, not a call result |
Thinking @dec is a "marker" and f stays the same | The name f is rebound to the result of dec(f); the original is reachable only via the closure or __wrapped__ |
| Writing a parametrized decorator with two levels | @foo('x') calls foo('x') and applies its result — two levels are not enough, a third is required |
Forgetting @functools.wraps(func) on the wrapper | __name__ becomes wrapper, the docstring and signature are lost — help, tracebacks and autodoc break |
Reading a @a / @b stack top-down as the application order | Application runs bottom-up — a(b(f)); top-down is only the order the wrappers are entered at call time |
Not forwarding *args, **kwargs through the wrapper | The wrapper only works with the signature its author happened to guess, and fails on any other call |
Treating only a def-defined function as a decorator | A class with __call__ and a lambda are equally valid decorators — the single requirement is that the object is callable |
What interviews check
The topic is part of the mandatory minimum for a middle developer, and it is probed almost identically everywhere. First you are asked what @dec expands into — answering "it is f = dec(f)" closes half of the follow-ups, while answering "it is a marker for the interpreter" turns the rest of the conversation into a review of your mistakes. Then you are asked to write a timing decorator, where the interviewer watches for *args, **kwargs, for returning the result, and for @wraps. A missing wraps is the most common complaint because it is visible from the outside — f.__name__ will report wrapper.
Two evaluation-time questions follow. The first is the difference between @foo and @foo() — the candidate should name the three nesting levels and explain why there are exactly three. The second is stacking order: given a snippet with two decorators, predict the result; a correct answer distinguishes the order of application (bottom-up) from the order the wrappers are entered at call time (top-down). The classic mistake is quoting one order for both. When the topic goes deeper, expect questions on decorating classes, on a decorator class with __call__, and on __wrapped__ with inspect.signature — by then the interviewer is checking whether you have actually used introspection yourself.