Maintenance

Site is under maintenance — quizzes are still available.

Go to quizzes
Python

The Hidden Trap of Decorator Chaining in Python

Understanding Python decorator chaining: why decorators applied bottom-to-top execute top-to-bottom, with practical examples and a real-world lesson from production API code.

July 2026 4 min read 1 views 0 hearts

The Hidden Trap of Decorator Chaining That Even Experienced Python Developers Miss

Python decorators are one of those features that feel magical when you first understand them. That @ syntax sitting above a function definition looks clean and elegant. But when you start chaining multiple decorators together, the magic can quickly turn into confusion if you don't understand what's actually happening underneath.

Let me walk you through a situation I've seen trip up developers at PythonSkillset more times than I can count.

How Decorators Actually Work (The Simple Version)

Before we dive into chaining, let's recap what a decorator really does. When you write:

@decorator
def my_function():
    pass

Python is essentially doing this:

def my_function():
    pass
my_function = decorator(my_function)

The decorator takes your function, wraps it somehow, and returns a new function that replaces the original. Simple enough.

Where Chaining Gets Tricky

Now watch what happens with multiple decorators:

@decorator_a
@decorator_b
@decorator_c
def my_function():
    pass

Most people intuitively think this applies decorator_a first, then decorator_b, then decorator_c. But that's not how it works.

Python processes decorators from bottom to top. The actual execution order is:

  1. decorator_c wraps the original function
  2. decorator_b wraps the result from step 1
  3. decorator_a wraps the result from step 2

So when you call my_function(), it runs through this chain in reverse order:

  1. decorator_a logic runs first
  2. Then decorator_b logic
  3. Then decorator_c logic
  4. Finally, your original function runs

A Real Example That Makes This Concrete

Let me show you a practical example that demonstrates this clearly. Say you're building an API endpoint at PythonSkillset that needs authentication AND logging AND rate limiting:

def require_auth(func):
    def wrapper(*args, **kwargs):
        # Check authentication here
        print("Checking authentication...")
        return func(*args, **kwargs)
    return wrapper

def log_request(func):
    def wrapper(*args, **kwargs):
        print(f"Logging request to {func.__name__}")
        return func(*args, **kwargs)
    return wrapper

def rate_limit(func):
    def wrapper(*args, **kwargs):
        print("Checking rate limit...")
        return func(*args, **kwargs)
    return wrapper

@require_auth
@log_request
@rate_limit
def get_user_data(user_id):
    return f"Data for user {user_id}"

When you call get_user_data(42), here's what actually prints:

Checking authentication...
Logging request to get_user_data
Checking rate limit...

Wait - that seems backwards, right? The rate limit check should happen before logging. And authentication should come first.

This is exactly the kind of pitfall that catches developers off guard. The decorators are applied from bottom to top (rate_limit first, then log_request, then require_auth), but they execute from top to bottom when called.

The Ordering Rule You Need to Remember

Here's the mental model I use: Think of decorator stacking like placing blankets on a bed. The first blanket you put down (bottom decorator) is closest to the mattress. The last blanket (top decorator) is on top and gets touched first when someone sits down.

In practical terms:

  • Decorators applied first (bottom of the stack) run LAST when the function is called
  • Decorators applied last (top of the stack) run FIRST when the function is called

A Common Workaround That Actually Works

If you need decorators to execute in a specific order, you have two options:

Option 1: Manual stacking in the order you want execution

# This will execute decorator_c first, then decorator_b, then decorator_a
decorator_a(decorator_b(decorator_c(my_function)))

Option 2: Design your decorators to be order-independent

This is usually the better approach. Make each decorator do one thing well and don't rely on execution order. For example:

from functools import wraps

def log_request(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        print(f"Called {func.__name__}")
        return result
    return wrapper

def add_timing(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        import time
        start = time.time()
        result = func(*args, **kwargs)
        print(f"Took {time.time() - start:.2f}s")
        return result
    return wrapper

@log_request
@add_timing
def compute_something():
    # Does some work
    return "done"

In this case, the order doesn't matter much because each decorator does its own thing independently.

The Real-World Lesson

At PythonSkillset, we learned this lesson the hard way when a deployment went wrong because authentication decorators were executing after logging decorators in our production API. The logs showed sensitive information before authentication had a chance to reject unauthorized requests.

The fix was simple once we understood the order: we moved the authentication decorator to the bottom of the stack where it would execute first when the function was called.

# This executes: auth first, then logging, then rate limiting
@rate_limit      # Runs third
@log_request     # Runs second  
@require_auth    # Runs first
def get_user_data(user_id):
    return f"Data for user {user_id}"

It feels counterintuitive at first, but once you internalize that decorators are applied bottom-to-top but execute top-to-bottom, you'll never get tripped up again.

Next time you're stacking decorators, take a moment to trace through the execution order. It might save you from a tricky debugging session later.

Comments

Questions, corrections, and tips stay visible for everyone reading this page.

0 in thread

Join the discussion

Shown next to your comment.

Up to 4,000 characters

No comments yet

Be the first to leave a note — it helps the next reader.