Skip to content
Python

Python Decorators Explained (With Examples)

By The EbookWale Team · Updated June 16, 2026 · 4 min read

A decorator is a function that wraps another function to add behaviour without changing its code. Here's how Python decorators work, the @ syntax, functools.wraps, decorators with arguments, and real-world uses.

A decorator is a function that wraps another function to add behaviour — logging, timing, caching, access checks — without touching the original’s code. The @decorator line you put above a function is just shorthand for “pass this function through the decorator and use the result instead.”

Decorators look like magic until you see the one idea underneath: in Python, functions are values you can pass around and return. Once that clicks, decorators are simple — and they’re a guaranteed intermediate-Python interview topic.

The foundation: functions are first-class

In Python you can store a function in a variable, pass it as an argument, and return it from another function:

def shout(text):
    return text.upper()

f = shout              # store it
print(f("hi"))         # "HI"

A decorator uses all three of those abilities at once.

A decorator, built by hand

A decorator takes a function, defines a wrapper that adds behaviour around it, and returns the wrapper:

def logged(func):
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__}")
        result = func(*args, **kwargs)
        print("Done")
        return result
    return wrapper

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

add = logged(add)      # wrap it manually
add(2, 3)              # prints the logs, returns 5

That add = logged(add) line is the whole concept. The @ syntax is just sugar for it.

The @ syntax

These two are identical:

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

# ...is exactly the same as:
def add(a, b):
    return a + b
add = logged(add)

The @logged line runs at definition time and replaces add with the wrapped version. The *args, **kwargs in the wrapper let it pass through any arguments, so one decorator works on any function.

add() add() wrapper log before… log after… @logged
A decorator wraps a function in new behaviour, unchanged inside.
🔑 REMEMBER — @decorator above def func means exactly func = decorator(func). There is no magic — just a function being passed through another function and reassigned.

Preserve identity with functools.wraps

Wrapping hides the original function’s name and docstring behind the wrapper’s. functools.wraps copies them back:

import functools

def logged(func):
    @functools.wraps(func)         # keep func's name & docstring
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

Always use @functools.wraps in your decorators — without it, add.__name__ reports "wrapper" and debugging gets confusing.

Decorators with arguments

To configure a decorator (e.g. “retry 3 times”), add one more layer — a function that returns a decorator:

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 greet():
    print("hi")

@repeat(times=3) calls repeat first, which returns the actual decorator. Three layers feels like a lot — but it’s the same wrap-and-return idea, nested once.

Real-world decorators you’ll meet

  • @functools.lru_cache — caches a function’s results so repeat calls are instant. Turns the slow recursive Fibonacci into a fast one (the same memoization idea from recursion).
  • @property, @staticmethod, @classmethod — built-in decorators that change how methods behave (see Python OOP).
  • @app.route("/") — Flask/FastAPI register URL handlers with decorators.
  • Timing, logging, authentication, retries — cross-cutting concerns you add without editing the function itself.

Common mistakes

  • Forgetting *args, **kwargs in the wrapper, so the decorator only works on no-argument functions.
  • Omitting functools.wraps, losing the function’s name and docstring.
  • Forgetting to return func’s result from the wrapper, so decorated functions silently return None.
  • Confusing the layers in a parameterised decorator — remember it’s “function returns decorator returns wrapper.”
⚠️ GOTCHA — If a decorated function suddenly returns None, your wrapper almost certainly forgot return func(*args, **kwargs). The wrapper must return the wrapped call's result.

Where this fits

Decorators are part of Phase 5 — the Pythonic intermediate layer — in our Python roadmap, and they build directly on first-class functions and OOP (@property is a decorator).

They’re worked through with diagrams in Python in Three Months, the job-ready tier, and introduced via @property in Python in One Month. For writing robust library-grade decorators, contextlib, and the descriptor protocol underneath @property, Python for Staff Engineers goes deeper.

See @decorator as func = decorator(func) and the magic evaporates.

Frequently asked questions

What is a decorator in Python?

A decorator is a function that takes another function, wraps it to add behaviour (like logging, timing, or caching), and returns the wrapped version. The @decorator syntax above a function definition is shorthand for reassigning the function to its decorated form.

What does the @ symbol do in Python?

The @ above a function or method applies a decorator. Writing @my_decorator before def func is exactly the same as writing func = my_decorator(func) after the definition — it replaces the function with the decorator's wrapped version.

Why use functools.wraps in a decorator?

Wrapping a function replaces its name, docstring, and metadata with the wrapper's. functools.wraps copies the original function's identity onto the wrapper, so the decorated function still reports its real name and docstring — important for debugging and tools.

What are common real-world uses of decorators?

Caching results (@functools.lru_cache), timing and logging, access control and authentication, registering routes in web frameworks (@app.route in Flask), retry logic, and turning methods into properties (@property) or static/class methods (@staticmethod, @classmethod).