Python Decorators Deep Dive: Syntax, Stacking, and Interview Patterns
Understand what decorators really do under the hood, how stacking multiple decorators works, and how to trace through decorated function calls in interviews.
Ameer Abdullah
Data Science Graduate · AI/ML & Data Science
A decorator is syntactic sugar for passing a function as an argument to another function and replacing the original with the result. The @ syntax is a convenience - it does nothing you couldn't do with an explicit assignment. Understanding decorators requires understanding that functions in Python are first-class objects that can be passed, returned, and stored like any other value.
Desugaring the @ Syntax
# These two are exactly equivalent:
@my_decorator
def greet():
print('Hello')
# Equivalent to:
def greet():
print('Hello')
greet = my_decorator(greet)When Python encounters @my_decorator above a def statement, it calls my_decorator(greet) and binds the result back to the name 'greet'. If my_decorator returns a different function (the wrapper), then greet now refers to that wrapper. Calling greet() calls the wrapper, not the original function body - unless the wrapper explicitly calls the original.
Writing a Decorator from Scratch
import functools
def log_calls(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f'Calling {func.__name__}')
result = func(*args, **kwargs)
print(f'{func.__name__} returned {result}')
return result
return wrapper
@log_calls
def add(a, b):
return a + b
add(3, 4)Output: 'Calling add', then 'add returned 7'. The wrapper uses *args and **kwargs to accept any arguments and forward them to the original function. functools.wraps(func) copies the original function's __name__, __doc__, and other metadata to the wrapper, so decorated functions look like the original when inspected.
Stacking Decorators: Order Matters
@decorator_A
@decorator_B
def func():
pass
# Equivalent to:
func = decorator_A(decorator_B(func))Decorators are applied bottom-up (inner to outer), but called top-down (outer to inner). decorator_B is applied first (wrapping the original func), then decorator_A wraps the result. When func() is called, decorator_A's wrapper runs first, which calls decorator_B's wrapper, which calls the original function. Many interview questions test this ordering with print statements in each decorator.
Parameterized Decorators
def repeat(n):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for _ in range(n):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(3)
def say_hi():
print('Hi')
say_hi() # prints Hi three timesA parameterized decorator is a function that returns a decorator (which itself returns a wrapper). There are three layers of nesting. @repeat(3) first calls repeat(3), which returns decorator. That decorator is then applied to say_hi: say_hi = decorator(say_hi). The resulting wrapper calls the original say_hi n times. Interview questions often ask you to trace through all three layers of nesting.
Quick Concept Quiz
If you stack @A and @B decorators (with @A on top), in what order are they applied and called?
Ready to test your execution tracing skills?
Hop directly into the Python trace map to start coding, grading queries, and logging XP metrics to your workspace profile.