How do decorators work under the hood in Python?
A decorator in Python is a callable -- typically a function -- that takes a function as an argument and returns a new callable. The @decorator syntax is pure syntactic sugar. Writing @log_calls above a function definition is exactly equivalent to writing f = log_calls(f) after the def statement. The decorator is applied at class or module load time, not at call time.
The most common pattern is a wrapper function. The decorator defines an inner function (the wrapper) that calls the original function, adding behavior before or after -- logging, timing, authentication, retry logic. The decorator returns the wrapper. From the caller's perspective, they are calling the original function, but they are actually calling the wrapper.
A critical implementation detail: without functools.wraps, the wrapper function replaces the original function's __name__, __doc__, and other attributes. This breaks introspection tools, documentation generators, and test frameworks that rely on function names. functools.wraps(original) applied to the wrapper copies these attributes from the original to the wrapper. Always use it in production decorators.
Decorators can also be parameterized -- meaning the decorator itself takes arguments. In this case, you need an extra level of nesting: a factory function that takes the parameters and returns the actual decorator. For example, @retry(max_attempts=3) is a call to retry(max_attempts=3) which returns a decorator, which is then applied to the function. This is three levels: factory, decorator, wrapper.
Class-based decorators are also valid. A class that implements __call__ can be used as a decorator. This is useful when the decorator needs to maintain state across calls such as a call counter.
Decorators stack. When you apply multiple decorators, they are applied bottom-up: the one closest to the def runs first, wrapping the function, then the outer decorator wraps the result. Order matters -- a caching decorator inside an authentication decorator caches authenticated results; reversed, it caches before checking auth.
Common real-world uses: @staticmethod and @classmethod (built-in), @property, @functools.lru_cache, Flask's @app.route, and authentication or rate-limiting middleware in web frameworks.
Candidate correctly explains the @ syntax as sugar for f = decorator(f) and implements a basic wrapper, but does not know functools.wraps, parameterized decorators (triple nesting), or stacking order.
Candidate explains the desugaring, implements a working wrapper with functools.wraps, explains parameterized decorators (factory then decorator then wrapper), describes class-based decorators, and explains stacking order (bottom-up application).
Reading the answer is step one. Explaining it unprompted — under interview pressure — is what actually matters. Get AI-graded feedback on your answer with follow-up probes on your weak points.
Get Graded — Free Assessment