Skip to content

How to Use Nested Decorators in Python

Learn how to apply multiple decorators in Python (stacked decorators) to combine logging, timing, authentication, and other behaviors while understanding execution order and best practices.

Decorators are one of Python's most powerful features: they allow you to modify or extend the behavior of a function without changing its source code. Nested decorators (also called stacked decorators) take this a step further by applying multiple decorators to a single function, combining several modifications into a clean, reusable pattern.

This technique is widely used in web frameworks like Flask and Django (for authentication, logging, and caching), and in general Python programming for composing reusable behavior.

In this guide, you will learn how nested decorators work, understand the order of execution, and see practical examples ranging from simple to advanced.

What Is a Decorator?

A decorator is a function that takes another function as input, adds some behavior, and returns a modified version (a wrapper function):

def my_decorator(func):
    def wrapper():
        print("Before the function call")
        func()
        print("After the function call")
    return wrapper


@my_decorator
def greet():
    print("Hello!")


greet()

Output:

Before the function call
Hello!
After the function call

Applying Multiple Decorators (Nesting)

You can stack multiple decorators on a single function by placing them above the function definition. Each decorator wraps the result of the one below it:

@decorator_a
@decorator_b
def my_function():
    ...

This is equivalent to:

my_function = decorator_a(decorator_b(my_function))

Execution Order: Bottom to Top

Nested decorators are applied from bottom to top: the decorator closest to the function is applied first, and the outermost decorator is applied last.

@decorator_a    (applied second: outermost layer)
@decorator_b    (applied first: innermost layer)
def function():
    ...

Basic Example: HTML Tag Wrapping

Here is a classic example that wraps a function's output with HTML tags:

def italic(func):
    def wrapper():
        return '<i>' + func() + '</i>'
    return wrapper


def strong(func):
    def wrapper():
        return '<strong>' + func() + '</strong>'
    return wrapper


@italic
@strong
def introduction():
    return 'This is a basic program'


print(introduction())

Output:

<i><strong>This is a basic program</strong></i>

Step-by-Step Breakdown

  1. @strong is applied first (bottom): wraps the output in <strong>...</strong>.
  2. @italic is applied second (top): wraps the already-wrapped output in <i>...</i>.

The result reads inside-out: <i><strong>This is a basic program</strong></i>.

If you reverse the decorator order:

def italic(func):
    def wrapper():
        return '<i>' + func() + '</i>'
    return wrapper


def strong(func):
    def wrapper():
        return '<strong>' + func() + '</strong>'
    return wrapper


@strong
@italic
def introduction():
    return 'This is a basic program'

print(introduction())

Output:

<strong><i>This is a basic program</i></strong>

Now <italic> is the inner wrapper and <strong> is the outer one.

Handling Functions With Arguments

When decorating functions that accept arguments, use *args and **kwargs in the wrapper to pass them through:

def uppercase(func):
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        return result.upper()
    return wrapper


def exclaim(func):
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        return result + "!!!"
    return wrapper


@uppercase
@exclaim
def greet(name):
    return f"Hello, {name}"


print(greet("Alice"))

Output:

HELLO, ALICE!!!

Order of execution:

  1. greet("Alice") returns "Hello, Alice".
  2. @exclaim adds "!!!" -> "Hello, Alice!!!".
  3. @uppercase converts to uppercase -> "HELLO, ALICE!!!".

Practical Example: Logging and Timing

Nested decorators shine in real-world applications where you want to compose independent concerns:

import time
from functools import wraps


def log_call(func):
    """Log when a function is called and what it returns."""
    @wraps(func)
    def wrapper(*args, **kwargs):
        print(f"[LOG] Calling {func.__name__}({args}, {kwargs})")
        result = func(*args, **kwargs)
        print(f"[LOG] {func.__name__} returned {result}")
        return result
    return wrapper


def measure_time(func):
    """Measure and print the execution time of a function."""
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        elapsed = time.perf_counter() - start
        print(f"[TIME] {func.__name__} took {elapsed:.4f}s")
        return result
    return wrapper


@log_call
@measure_time
def compute_sum(n):
    """Compute the sum of numbers from 1 to n."""
    return sum(range(1, n + 1))


result = compute_sum(1_000_000)
print(f"Result: {result}")

Output:

[LOG] Calling compute_sum((1000000,), {})
[TIME] compute_sum took 0.0209s
[LOG] compute_sum returned 500000500000
Result: 500000500000

The @measure_time decorator wraps the function first (measures execution), and @log_call wraps it second (logs the call and result, including the timing).

Practical Example: Authentication and Authorization

In web applications, nested decorators are commonly used to enforce access control:

from functools import wraps

current_user = {"name": "Alice", "is_authenticated": True, "role": "admin"}


def require_auth(func):
    """Ensure the user is authenticated."""
    @wraps(func)
    def wrapper(*args, **kwargs):
        if not current_user.get("is_authenticated"):
            return "Error: Authentication required."
        return func(*args, **kwargs)
    return wrapper


def require_role(role):
    """Ensure the user has the required role."""
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            if current_user.get("role") != role:
                return f"Error: '{role}' role required."
            return func(*args, **kwargs)
        return wrapper
    return decorator


@require_auth
@require_role("admin")
def delete_user(user_id):
    return f"User {user_id} deleted successfully."


print(delete_user(42))

Output:

User 42 deleted successfully.

Order of checks:

  1. @require_role("admin") checks if the user has the admin role.
  2. @require_auth checks if the user is authenticated.

Since decorators apply bottom-to-top, but execute top-to-bottom at call time, authentication is checked first, then authorization.

Visualizing the Execution Order

Understanding nested decorator execution can be confusing. Here is a visual example:

def decorator_a(func):
    def wrapper():
        print("A: before")
        func()
        print("A: after")
    return wrapper


def decorator_b(func):
    def wrapper():
        print("B: before")
        func()
        print("B: after")
    return wrapper


@decorator_a
@decorator_b
def say_hello():
    print("Hello!")


say_hello()

Output:

A: before
B: before
Hello!
B: after
A: after

The execution forms nested layers, like an onion:

decorator_a wrapper (outer)
    -> decorator_b wrapper (inner)
        -> original function
    <- decorator_b wrapper returns
<- decorator_a wrapper returns

Common Mistake: Forgetting Parentheses on Parameterized Decorators

When a decorator takes arguments (like @require_role("admin")), it needs an extra layer of nesting:

def require_role(func):
    def wrapper(*args, **kwargs):
        ...
    return wrapper

@require_role("admin")  # TypeError: require_role() got an unexpected argument
def my_function():
    pass

Fix

Use a decorator factory (a function that returns a decorator):

def require_role(role):           # Accepts the parameter
    def decorator(func):          # Accepts the function
        def wrapper(*args, **kwargs):  # Replaces the function
            return func(*args, **kwargs)
        return wrapper
    return decorator

@require_role("admin")  # Works correctly
def my_function():
    pass

Summary: Key Rules for Nested Decorators

Rule Description
Application order Bottom to top: the decorator closest to the function is applied first
Execution order Top to bottom: the outermost wrapper executes first at call time
Use @wraps Always use functools.wraps to preserve function metadata
Use *args, **kwargs Pass all arguments through wrappers to support any function signature
Parameterized decorators Use a three-level nesting pattern (factory -> decorator -> wrapper)

Conclusion

Nested decorators in Python let you compose multiple independent behaviors onto a single function in a clean, readable way.

They apply from bottom to top during decoration but execute from top to bottom at call time, forming layered wrappers around the original function.

By following best practices (using @functools.wraps, accepting *args and **kwargs, and keeping each decorator focused on a single concern), you can build powerful, reusable decorator stacks for logging, timing, authentication, caching, and much more.

Navigation