Python · Functions

Python Decorators Explained — Wrapping Functions, functools.wraps, and Decorators with Arguments

5 min read Updated 2026-06-19

Practice Decorators interview questions

Python decorators, explained

A decorator is one of Python's most distinctive features — and one interviewers reach for constantly, because understanding it requires that functions are first-class objects, that you grasp closures, and that you know what the @ syntax actually does. This guide builds the idea up from scratch and works through the patterns that trip people up: preserving metadata, parameterised decorators, class-based decorators, and stacking order.

What a decorator actually is

A decorator is just a callable that takes a function and returns a function (usually a new one that wraps the original). Because functions are first-class objects in Python, you can pass them around, define them inside other functions, and return them.

def log_calls(func):
    def wrapper(*args, **kwargs):
        print(f"calling {func.__name__}")
        result = func(*args, **kwargs)   # call the original
        print(f"{func.__name__} returned {result!r}")
        return result
    return wrapper                       # return the wrapped version

wrapper is a closure: it remembers func from the enclosing scope even after log_calls has returned. The *args, **kwargs forwarding is what lets one wrapper handle a function with any signature.

The @ syntax is just sugar

The @decorator line above a function is shorthand for reassigning the name to the decorated version. These two snippets are identical:

@log_calls
def add(a, b):
    return a + b

# is exactly the same as:
def add(a, b):
    return a + b
add = log_calls(add)     # the @ line does this

So add no longer points at the original function — it points at wrapper. That's the whole trick: decoration happens once, at definition time, and every later call to add actually runs wrapper.

Preserving metadata with functools.wraps

There's a hidden cost to the version above: add.__name__ is now "wrapper" and its docstring is gone, because the name is bound to the wrapper, not the original. This breaks introspection, debuggers, and documentation tools.

import functools

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

@log_calls
def add(a, b):
    """Add two numbers."""
    return a + b

add.__name__   # 'add'  (without @wraps it would be 'wrapper')
add.__doc__    # 'Add two numbers.'

Always use functools.wraps on the wrapper. It's the single most common thing missing from a hand-written decorator.

Decorators that take arguments

To configure a decorator (e.g. @retry(times=3)), you need one more layer: a function that takes the arguments and returns a decorator. So a parameterised decorator is a function returning a function returning a function.

import functools

def retry(times):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(times):
                try:
                    return func(*args, **kwargs)
                except Exception:
                    if attempt == times - 1:
                        raise            # last attempt — give up
        return wrapper
    return decorator

@retry(times=3)          # retry(3) runs first, returns the real decorator
def fetch():
    ...

Read it outside-in: retry(times=3) is called immediately and returns decorator, which is then applied to fetch. That extra layer is the only structural difference from a plain decorator.

Class-based decorators

A class works as a decorator if its instances are callable (it defines __call__). This is handy when the decorator needs to hold state across calls.

import functools

class CountCalls:
    def __init__(self, func):
        functools.update_wrapper(self, func)   # the @wraps equivalent for classes
        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 greet():
    print("hi")

Here greet becomes a CountCalls instance; calling greet() runs __call__. Use a class when state (like count) is cleaner as an attribute than as a closure variable.

Stacking decorators and order

Multiple decorators apply bottom-up — the one closest to the function wraps first, and the topmost runs outermost at call time.

@bold          # applied last, runs first at call time (outermost)
@italic        # applied first, wraps the original
def text():
    return "hi"

# equivalent to:
text = bold(italic(text))

So the decoration order is bottom-to-top, but the execution order at call time is top-to-bottom (outermost first). Getting this backwards is a classic interview slip.

Where decorators earn their keep

Real-world decorators centralise cross-cutting concerns so the wrapped function stays focused:

import functools, time

def timed(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        try:
            return func(*args, **kwargs)
        finally:
            print(f"{func.__name__} took {time.perf_counter() - start:.4f}s")
    return wrapper

Common uses: functools.lru_cache for memoisation, @property for computed attributes, framework routing (@app.route(...)), authentication checks, logging, timing, and retries — anywhere you'd otherwise copy-paste boilerplate around many functions.

Recap

A decorator is a callable that takes a function and returns a (usually wrapping) function, and @deco is just sugar for func = deco(func) at definition time. Forward *args, **kwargs so the wrapper handles any signature, and always apply functools.wraps to preserve the original's name and docstring. A decorator that takes arguments needs an extra layer (a function returning a decorator); a class with __call__ works when you want stateful decoration. Stacked decorators apply bottom-up and execute outermost-first. Master the closure underneath and decorators stop feeling like magic.

Practice tests are coming soon

Get notified when interactive mock interviews and quizzes launch.