datarekha
Python Medium Asked at GoogleAsked at AmazonAsked at MetaAsked at MicrosoftAsked at Uber

What is a decorator in Python, and how does it work under the hood?

The short answer

A decorator is a callable that takes a function, wraps it with extra behaviour, and returns the new callable. The @syntax is syntactic sugar for reassigning the function name to the wrapper immediately after definition.

How to think about it

A decorator sounds mysterious until you see the one line it stands for. The @decorator syntax does nothing more than this: right after a function is defined, Python calls decorator(your_function) and rebinds the name to whatever comes back. So a decorator is simply a callable that takes a function and returns a (usually wrapped) function. The interviewer wants you to say that plainly — and then build one — rather than wave at “it adds functionality.”

Here is the equivalence in full:

@decorator
def greet(name): ...

# is exactly:
def greet(name): ...
greet = decorator(greet)

Python evaluates decorator(greet) the moment the def finishes and binds greet to the result. If the decorator returns a wrapper that closes over the original, every later call to greet now runs the wrapper instead.

A worked example

Let’s make a @trace decorator that announces each call and what it returned. The wrapper takes *args, **kwargs so it works on any function, and functools.wraps copies the original’s identity across so it isn’t lost:

import functools

def trace(func):
    @functools.wraps(func)            # keep func's __name__, __doc__, __wrapped__
    def wrapper(*args, **kwargs):
        print(f"-> calling {func.__name__}{args}")
        result = func(*args, **kwargs)
        print(f"<- {func.__name__} returned {result}")
        return result
    return wrapper

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

# @trace is exactly: add = trace(add)
print("result:", add(2, 3))
print("name preserved:", add.__name__)    # 'add', not 'wrapper'
print("doc preserved :", add.__doc__)

# A decorator that takes its own arguments just needs one more layer:
def repeat(times):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for _ in range(times):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

@repeat(times=3)
def say(msg):
    print(msg)

say("hello")
-> calling add(2, 3)
<- add returned 5
result: 5
name preserved: add
doc preserved : Add two numbers.
hello
hello
hello

Two things to notice. The wrapper ran around add — printing before and after — because add now points at wrapper. And add.__name__ still says add, not wrapper, only because of functools.wraps; drop it and the wrapper’s identity leaks through, breaking logging and introspection.

Stacking decorators

Stack them and they apply bottom-up — the one nearest the def wraps first:

@timer
@repeat(times=3)
def fetch_data(url): ...
# equivalent to: fetch_data = timer(repeat(times=3)(fetch_data))

The idea underneath

A decorator is just a higher-order function: a function that takes a function and returns a function, with @ as pure syntactic sugar for the rebind. Hold that one sentence and every decorator — class-based ones, ones that take arguments, stacked ones — becomes something you can reason out from first principles.

Learn it properly Decorators

Keep practising

All Python questions

Explore further

Skip to content