Decorators Interview Questions & Answers

5 questions Updated 2026-06-18

Python interview questions on decorators, functools.wraps, decorators with arguments, class-based decorators, stacking order, and real-world use cases.

Read the in-depth guidePython Decorators Explained — Wrapping Functions, functools.wraps, and Decorators with Arguments

A decorator is a callable that takes a function and returns a (usually wrapped) function, letting you add behaviour without modifying the original. The @decorator syntax above a def is just sugar for reassigning the name to the decorator's result: func = decorator(func).

def log_calls(func):
    def wrapper(*args, **kwargs):    # accept any signature
        print(f"calling {func.__name__}")
        return func(*args, **kwargs) # delegate to the original
    return wrapper

@log_calls
def add(a, b):
    return a + b
# equivalent to: add = log_calls(add)

add(2, 3)   # prints "calling add", returns 5

This works because functions are first-class objects — they can be passed around and returned. Decorators are the idiomatic way to factor out cross-cutting concerns (logging, timing, caching, access control).

Without it, the wrapper replaces the original function's identity: the decorated object reports the wrapper's __name__, __doc__, signature, and __module__, which breaks introspection, debugging, and tools that rely on metadata. functools.wraps copies that metadata from the original onto the wrapper.

import functools

def log_calls(func):
    @functools.wraps(func)       # copy name, docstring, __wrapped__, etc.
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@log_calls
def greet():
    "say hello"
    ...

greet.__name__   # "greet"  (without wraps -> "wrapper")
greet.__doc__    # "say hello"

It also sets __wrapped__, so inspect.signature and unwrapping still work. Rule of thumb: always apply @functools.wraps(func) to your wrapper — it's effectively free and prevents subtle bugs.

You add another layer of nesting: an outer function takes the decorator's arguments and returns the actual decorator, which takes the function and returns the wrapper. So @repeat(3) first calls repeat(3) to get a decorator, which is then applied to the function.

import functools

def repeat(n):                       # takes the decorator argument
    def decorator(func):             # takes the function
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for _ in range(n):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

@repeat(3)                           # repeat(3) returns 'decorator'
def ping():
    print("pong")

The mental model: @repeat(3) is ping = repeat(3)(ping) — three calls deep. Remember the parentheses: @repeat(3) (with args) differs from @repeat (passing the function directly), and forgetting them is a common bug.

A class becomes a decorator by being callable — define __call__. The __init__ receives the decorated function; __call__ runs the wrapping logic on each invocation. This is handy when the decorator needs to hold state (like a call count) in a clean, attribute-based way.

import functools

class CountCalls:
    def __init__(self, func):
        functools.update_wrapper(self, func)  # the class-based wraps
        self.func = func
        self.count = 0
    def __call__(self, *args, **kwargs):
        self.count += 1
        print(f"call #{self.count}")
        return self.func(*args, **kwargs)

@CountCalls
def hello():
    print("hi")

hello(); hello()      # "call #1" then "call #2"
hello.count           # 2 — state lives on the instance

Use functools.update_wrapper (the function-form of wraps) to preserve metadata. Class decorators shine for stateful decorators; for simple stateless ones, a nested function with a nonlocal closure is usually lighter.

Decorators apply bottom-up (nearest the function first) at definition time, but the resulting wrappers execute top-down at call time. Stacking is just nested application: the top decorator wraps the result of the ones below it.

@a
@b
def f(): ...
# equivalent to: f = a(b(f))   — b wraps first, a wraps outermost

def bold(fn):
    return lambda: "<b>" + fn() + "</b>"
def italic(fn):
    return lambda: "<i>" + fn() + "</i>"

@bold
@italic
def text():
    return "hi"

text()   # "<b><i>hi</i></b>"  — bold is outer, runs around italic

So the closest decorator is applied first but its logic runs innermost. Order matters whenever decorators have side effects or transform results — e.g. put @staticmethod outermost, or @app.route above @login_required so auth runs before the view.

Practice tests are coming soon

Get notified when interactive mock interviews and quizzes launch.