Interview Q&A / Python

IntermediateIn-depth3 min readUpdated 2 Oct 2026

What is a decorator in Python and how do you write one?

SHORT ANSWER

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

python
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)
# 90

The @timer line is exactly the same as writing calculate_fare = timer(calculate_fare) after the function.

How it works

  1. Python defines calculate_fare normally.
  2. It then calls timer(calculate_fare).
  3. timer defines wrapper, which remembers func through a closure, and returns it.
  4. The name calculate_fare now points to wrapper. 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.

python
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_required in Django.
  • Caching: @functools.lru_cache stores results of expensive calls.
  • Classes: @property, @staticmethod, @classmethod and @dataclass are all decorators.
  • Reliability: retrying a payment gateway call, rate limiting an API, logging every request.
  • Testing: @pytest.mark.parametrize and @patch from unittest.mock.

Common mistakes

  • Forgetting to return the result of func(...) inside the wrapper, so the decorated function returns None.
  • Forgetting to return wrapper from the decorator.
  • Skipping functools.wraps, which breaks debugging, docs and some frameworks that rely on the function name.
  • Writing @retry instead of @retry(3) for a decorator that expects arguments, or the other way round.
  • Calling the function by mistake: @timer() when timer takes 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.wraps do, 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.

#python#decorators#functions#closures

Related questions

Practise more Python questions

Test your recall with short answers, then return to the detailed explanations when you need context.

Open Python quick answers →

Get new questions like this by email

One short email when something new and useful lands. No spam, unsubscribe in one tap.

By subscribing you agree to the Privacy Policy.