What is a decorator in Python and how do you write one?
A decorator is a function that takes another function, wraps it with extra behaviour, and returns the wrapped version. Writing @my_decorator above a function is shorthand for func = my_decorator(func). They are used for logging, timing, caching and access checks without changing the original function's code.
Explain it simply
Think of gift wrapping. The gift inside does not change, but the wrapping adds something around it: a ribbon, a card, a "Happy Diwali" sticker. A decorator wraps a function the same way. The original function still does its job, and the decorator adds work before or after it, such as printing a log line, checking whether the user is logged in, or measuring how long it took.
This works because in Python functions are objects. You can pass a function into another function and return a new function from it.
Example
import functools
import time
def timer(func):
@functools.wraps(func) # keep the original name and docstring
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs) # call the original function
took = time.perf_counter() - start
print(f"{func.__name__} took {took:.4f}s")
return result
return wrapper
@timer
def calculate_fare(km):
return 30 + km * 12
print(calculate_fare(5))
# calculate_fare took 0.0000s (time varies)
# 90The @timer line is exactly the same as writing calculate_fare = timer(calculate_fare) after the function.
How it works
- Python defines
calculate_farenormally. - It then calls
timer(calculate_fare). timerdefineswrapper, which remembersfuncthrough a closure, and returns it.- The name
calculate_farenow points towrapper. Calling it runs the timing code, which calls the real function inside.
*args, **kwargs let the wrapper accept any arguments and pass them through, so one decorator works on many functions. functools.wraps copies the original function's __name__, __doc__ and other details onto the wrapper; without it, every decorated function would report its name as wrapper.
Decorators with arguments need one more layer: a function that takes the arguments and returns the actual decorator.
def retry(times):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(times):
try:
return func(*args, **kwargs)
except ConnectionError:
if attempt == times - 1:
raise
return wrapper
return decorator
@retry(times=3)
def check_payment_status(txn_id):
...Real-world use
- Web frameworks:
@app.route("/login")in Flask and@login_requiredin Django. - Caching:
@functools.lru_cachestores results of expensive calls. - Classes:
@property,@staticmethod,@classmethodand@dataclassare all decorators. - Reliability: retrying a payment gateway call, rate limiting an API, logging every request.
- Testing:
@pytest.mark.parametrizeand@patchfromunittest.mock.
Common mistakes
- Forgetting to
returnthe result offunc(...)inside the wrapper, so the decorated function returnsNone. - Forgetting to return
wrapperfrom the decorator. - Skipping
functools.wraps, which breaks debugging, docs and some frameworks that rely on the function name. - Writing
@retryinstead of@retry(3)for a decorator that expects arguments, or the other way round. - Calling the function by mistake:
@timer()whentimertakes no arguments.
What to say in the interview
"A decorator is a function that takes a function and returns a new function that adds behaviour around it. The @decorator syntax is just shorthand for reassigning the function to decorator(func). Inside, I write a wrapper that accepts *args and **kwargs, does something before and after calling the original, and returns its result. I always use functools.wraps so the wrapped function keeps its name and docstring. Common uses are logging, timing, caching with lru_cache, and access checks like Django's login_required. If a decorator needs its own arguments, I add one more outer function that returns the decorator."
Follow-up questions
- What does
functools.wrapsdo, and why is it needed? - How do you write a decorator that takes arguments?
- In what order are stacked decorators applied?
- Can a class be used as a decorator?
Go deeper
Read these once the basics above feel easy.
Stacked decorators apply from the bottom up. With @a above @b above def f, Python computes f = a(b(f)), so when you call f, a's wrapper runs first on the way in.
A class can act as a decorator if its instances are callable: __init__ receives the function and __call__ runs the wrapper logic. This is handy when the decorator needs to keep state, such as a call counter. Decorators can also be applied to classes themselves, which is how @dataclass adds __init__, __repr__ and __eq__ to a class.