Understanding How Stacking Decorators Affects the Final Function

Placing two or more @ lines above a function definition creates a decorator stack. Each decorator wraps the result of the one below it, forming a chain of nested function calls. The stacking order determines two things: which decorator wraps first (bottom to top), and which decorator's wrapper executes first when the function is called (top to bottom). Getting this order wrong can break metadata, produce unexpected behavior, or crash entirely—especially when built-in descriptors like @classmethod and @staticmethod are involved.

Application Order vs. Execution Order

When Python encounters stacked decorators, it applies them from bottom to top. The decorator closest to the def statement wraps the original function first. Each subsequent decorator (moving upward) wraps the result of the previous one. The Python Language Reference states that “multiple decorators are applied in nested fashion” and gives the equivalence directly:

import functools

def decorator_a(func):
    print(f"  Applying A to {func.__name__}")
    @functools.wraps(func)
    def a_wrapper(*args, **kwargs):
        print("A: before")
        result = func(*args, **kwargs)
        print("A: after")
        return result
    return a_wrapper

def decorator_b(func):
    print(f"  Applying B to {func.__name__}")
    @functools.wraps(func)
    def b_wrapper(*args, **kwargs):
        print("B: before")
        result = func(*args, **kwargs)
        print("B: after")
        return result
    return b_wrapper

print("Defining greet:")
@decorator_a
@decorator_b
def greet(name):
    """Say hello."""
    print(f"  Hello, {name}")
    return name

When Python processes this definition, the output shows bottom-to-top application:

Defining greet:
  Applying B to greet       # B wraps the original first
  Applying A to b_wrapper   # A wraps B's result

This is equivalent to writing greet = decorator_a(decorator_b(greet)). The innermost call happens first (decorator_b(greet) returns b_wrapper), then the outermost call wraps it (decorator_a(b_wrapper) returns a_wrapper).

But when the function is called, execution flows in the opposite direction—top to bottom, outermost to innermost:

greet("Ada")
# A: before           <-- A's wrapper runs first (outermost)
# B: before           <-- B's wrapper runs second
#   Hello, Ada        <-- original function runs last
# B: after            <-- B's "after" code runs
# A: after            <-- A's "after" code runs
Note

Think of decorator stacking like wrapping a gift in multiple layers of paper. The first layer of paper (bottom decorator, closest to the function) goes on first. The second layer (top decorator) goes on last. But when you unwrap the gift (call the function), you peel the outer layer first and work inward.

Python Pop Quiz
What is the equivalent longhand of this decorator stack?
@X
@Y
@Z
def func():
    pass
Incorrect. You have the nesting completely reversed. This reads as X wraps first, then Y, then Z -- but Python applies decorators bottom-to-top, meaning Z (closest to def) wraps first. The correct expansion puts Z innermost:
# Step by step:
step_1 = Z(func)   # Z wraps first (closest to def)
step_2 = Y(step_1) # Y wraps Z's result
step_3 = X(step_2) # X wraps Y's result
func = step_3       # equivalent to func = X(Y(Z(func)))
Correct. Z wraps first (bottom-to-top), then Y wraps the result, then X wraps that. The nested call reads inside-out: X(Y(Z(func))). Here is a concrete way to verify it:
def X(f): print(f"X wraps {f.__name__}"); return f
def Y(f): print(f"Y wraps {f.__name__}"); return f
def Z(f): print(f"Z wraps {f.__name__}"); return f

@X
@Y
@Z
def func(): pass
# Output:
# Z wraps func
# Y wraps func
# X wraps func
Not quite. This scrambles the nesting order. The rule is straightforward: the @ lines map directly to the nested call, top-to-bottom becoming outermost-to-innermost. X is on top so it is outermost, Z is on bottom so it is innermost:
# @X  ->  outermost call: X(...)
# @Y  ->  middle call:    Y(...)
# @Z  ->  innermost call: Z(func)
# Result: func = X(Y(Z(func)))

# NOT Y(X(Z(func))) -- the @ order is preserved
# in the nesting, reading top-to-bottom as outer-to-inner.

The @classmethod and @staticmethod Trap

@classmethod and @staticmethod are not regular function decorators. They are descriptors. A classmethod object does not have a __call__ method—it is not directly callable. It only becomes callable when Python's descriptor protocol triggers its __get__ method during attribute access on a class or instance. This distinction creates a specific trap when stacking custom decorators with @classmethod.

The Broken Order

import functools

def log_calls(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__}")
        return func(*args, **kwargs)
    return wrapper

class Service:
    # BROKEN: custom decorator wraps classmethod object
    @log_calls
    @classmethod
    def create(cls, name):
        return cls()

# Service.create("test")
# TypeError: 'classmethod' object is not callable

Here is what happens step by step. Python applies @classmethod first (bottom to top), which wraps the create function in a classmethod descriptor object. Then Python applies @log_calls, which receives the classmethod object as its func argument. When Service.create("test") is called, log_calls's wrapper executes func(*args, **kwargs)—but func is the classmethod object, which is not callable. The descriptor protocol's __get__ was never triggered because the custom decorator simply stored the object and tried to call it directly.

The Correct Order

class Service:
    # CORRECT: @classmethod wraps the custom decorator's result
    @classmethod
    @log_calls
    def create(cls, name):
        """Create a new Service instance."""
        return cls()

Service.create("test")
# Calling create
# <__main__.Service object at 0x...>

Now @log_calls wraps the raw create function first (a normal callable), producing wrapper. Then @classmethod wraps wrapper, which is also a normal callable. When Service.create("test") is called, the descriptor protocol triggers classmethod.__get__, which binds the class and returns a callable bound method. That bound method calls wrapper, which calls the original create. Everything works because @classmethod never has to wrap a non-callable descriptor.

Warning

Always place @classmethod and @staticmethod as the outermost (topmost) decorator. This is the convention established by PEP 318 when decorator syntax was introduced: @classmethod on the outside, custom decorators on the inside. The same rule applies to @staticmethod and @property. Note that @abstractmethod follows the opposite convention—it must be the innermost decorator (closest to def), placed below @classmethod or @staticmethod, as specified in the abc module documentation.

Python Pop Quiz
What does this code produce when you call Cache.clear()?
class Cache:
    @staticmethod
    @log_calls
    def clear():
        """Flush all entries."""
        return True
Correct. @log_calls wraps the raw clear function first (a normal callable), producing a wrapper. Then @staticmethod wraps that wrapper, which is also a normal callable. The descriptor protocol handles the rest at call time:
# This order works:
# 1. log_calls(clear) -> wrapper (callable)
# 2. staticmethod(wrapper) -> descriptor wrapping a callable
# At call time: descriptor.__get__() returns wrapper, which runs

# This order would BREAK:
# @log_calls
# @staticmethod    <- produces a staticmethod descriptor
# def clear(): ... <- log_calls receives a non-callable descriptor
Not this time. The TypeError trap only fires when the custom decorator is above @staticmethod. In this code, @staticmethod is outermost (correct position) and @log_calls is below it. So @log_calls wraps the raw function first, and @staticmethod wraps the resulting callable:
# The BROKEN order (custom decorator above @staticmethod):
@log_calls       # receives staticmethod object -- NOT callable!
@staticmethod
def clear(): ...

# The CORRECT order (this quiz's code):
@staticmethod    # receives log_calls' wrapper -- callable
@log_calls       # receives raw clear -- callable
def clear(): ...
Incorrect. @log_calls does fire. It wraps the function before @staticmethod does, so when Cache.clear() is called, the descriptor protocol returns log_calls' wrapper, which runs its logging code and then calls the original. You can verify:
import functools

def log_calls(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__}")
        return func(*args, **kwargs)
    return wrapper

class Cache:
    @staticmethod
    @log_calls
    def clear():
        return True

Cache.clear()  # prints: Calling clear

Metadata Propagation Through the Stack

When every decorator in a stack uses @functools.wraps(func), metadata propagates correctly through the entire chain. Each layer copies __name__, __doc__, __module__, __qualname__, __annotations__, and __type_params__ from whatever it receives (the full set defined by functools.WRAPPER_ASSIGNMENTS), and sets __wrapped__ pointing to the previous layer. The functools documentation confirms that update_wrapper automatically sets a __wrapped__ attribute on the wrapper pointing back to the original. The result is a chain of __wrapped__ references that inspect.unwrap() can follow all the way to the original function:

import functools
import inspect

def timer(func):
    @functools.wraps(func)
    def timer_wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return timer_wrapper

def logger(func):
    @functools.wraps(func)
    def logger_wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return logger_wrapper

@timer
@logger
def process(data):
    """Process incoming data."""
    return data

# Metadata is correct at the outermost level:
print(process.__name__)  # process
print(process.__doc__)   # Process incoming data.

# The __wrapped__ chain:
print(process.__wrapped__.__name__)             # process (logger's wrapper)
print(process.__wrapped__.__wrapped__.__name__)  # process (original)

# inspect.unwrap follows the chain:
original = inspect.unwrap(process)
print(original is process.__wrapped__.__wrapped__)  # True

If any decorator in the stack omits @functools.wraps, the chain breaks at that point. Every decorator above the broken link will copy the wrong metadata (the wrapper's name instead of the original's), and inspect.unwrap() will stop at the break because there is no __wrapped__ attribute to follow.

def broken_logger(func):
    # Missing @functools.wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@timer            # has @wraps -- copies from broken_logger's wrapper
@broken_logger    # missing @wraps -- chain breaks here
def process(data):
    """Process incoming data."""
    return data

print(process.__name__)  # wrapper -- wrong!
print(process.__doc__)   # None -- lost!
Python Pop Quiz
A three-decorator stack has @functools.wraps on layers 1 and 3 but not layer 2. What does inspect.unwrap() return?
Not quite. inspect.unwrap() follows the __wrapped__ chain one link at a time. If layer 2 has no __wrapped__ attribute, the traversal stops there and never reaches the original. The chain is only as strong as its weakest link:
import functools, inspect

def layer1(f):
    @functools.wraps(f)          # has @wraps
    def w(*a, **k): return f(*a, **k)
    return w

def layer2(f):
    def w(*a, **k): return f(*a, **k)  # NO @wraps
    return w

def layer3(f):
    @functools.wraps(f)          # has @wraps
    def w(*a, **k): return f(*a, **k)
    return w

@layer1
@layer2
@layer3
def target(): """Original."""

result = inspect.unwrap(target)
# result is layer2's wrapper -- chain broke there
print(hasattr(result, '__wrapped__'))  # False
Correct. inspect.unwrap() walks __wrapped__ links one at a time. Layer 1 has __wrapped__ pointing to layer 2's wrapper. But layer 2's wrapper has no __wrapped__ attribute (because @wraps was omitted), so the traversal stops there:
# Chain visualization:
# target (layer1's wrapper)
#   .__wrapped__ -> layer2's wrapper  (no __wrapped__)
#                    STOP -- inspect.unwrap returns this
#
# layer3's wrapper and the original are unreachable
No, ValueError is only raised when inspect.unwrap() detects a cycle (a wrapper chain that loops back to itself). A missing __wrapped__ simply terminates the traversal and returns whatever it stopped on:
# ValueError only on cycles:
def bad(f):
    def w(): pass
    w.__wrapped__ = w  # points to itself!
    return w

@bad
def oops(): pass
inspect.unwrap(oops)  # ValueError: wrapper loop
Spot the Bug
This code has a stacking order bug. The developer wants @log_calls to see every call attempt (including retries), but retries are not being logged. Which line contains the stacking order mistake?
@classmethod @retry(max_attempts=3) @log_calls @require_auth("admin") def generate(cls, report_type): """Generate a business report.""" return build_report(report_type)
Not quite. @classmethod is a descriptor and must always be outermost, so line 1 is correct. The problem is in the relationship between @retry and @log_calls. Think about which one needs to see the other's behavior.
Correct. @log_calls is inside @retry, so logging only sees the innermost call, not each retry attempt. To log every retry, @log_calls must be outside @retry: outermost decorators execute first and see everything that happens beneath them. The fix is to swap lines 2 and 3 so the stack reads @classmethod, @log_calls, @retry, @require_auth.
Not right. @require_auth being close to the function is intentional -- it rejects unauthorized users before any expensive work (retry, logging) happens. The bug is between @retry and @log_calls. Which one should see the other's behavior?

Debugging Stacked Decorators

When a decorator stack produces unexpected behavior, the most effective debugging technique is to inspect the __wrapped__ chain at runtime. Each layer of a properly decorated stack exposes exactly what it wraps, forming a traversable linked list from the outermost wrapper to the original function:

import inspect

def show_decorator_chain(func):
    """Walk the __wrapped__ chain and print each layer."""
    layer = 0
    current = func
    while True:
        qualname = getattr(current, '__qualname__', repr(current))
        print(f"  layer {layer}: {qualname}")
        if not hasattr(current, '__wrapped__'):
            break
        current = current.__wrapped__
        layer += 1

# Using the earlier example:
show_decorator_chain(process)
# layer 0: process  (actually timer_wrapper, renamed by @wraps)
# layer 1: process  (actually logger_wrapper, renamed by @wraps)
# layer 2: process  (the original function)

# To confirm the original:
original = inspect.unwrap(process)
print(f"Original function object: {original}")
print(f"Defined at: {inspect.getfile(original)}:{inspect.getsourcelines(original)[1]}")

When a decorator chain breaks—producing the wrong name, losing a docstring, or raising TypeError—this technique immediately reveals which layer is responsible. If the names stop changing at a certain layer, that is where @functools.wraps was omitted. If the chain terminates early, a decorator is not setting __wrapped__. If a classmethod object appears in the chain, the stacking order is wrong.

For decorators with arguments (parameterized decorators), the same principle applies but with one extra nesting level. A parameterized decorator like @retry(max_attempts=3) is a factory that returns the real decorator. The factory call happens first, then the returned decorator wraps the function. In a stack, this means:

# Parameterized decorator: the outer function is a factory
def retry(max_attempts=3):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            last_err = None
            for attempt in range(1, max_attempts + 1):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    last_err = e
            raise last_err
        return wrapper
    return decorator  # factory returns the real decorator

# In a stack, Python evaluates retry(3) first, gets back `decorator`,
# then applies `decorator` to the function -- same bottom-to-top rule.
@log_calls
@retry(max_attempts=3)
def fetch_data(url):
    """Fetch data from a remote API."""
    pass

# Equivalent to: fetch_data = log_calls(retry(max_attempts=3)(fetch_data))

The key insight is that parameterized decorators do not change the stacking rules. The factory call resolves to a plain decorator before stacking even begins. The @functools.wraps still goes inside the innermost wrapper, and the __wrapped__ chain still works exactly as it does with non-parameterized decorators.

Python Pop Quiz
When Python encounters @retry(max_attempts=3) in a decorator stack, what happens first?
Correct. Python evaluates the expression after @ first. Since retry(max_attempts=3) is a call expression, it executes immediately and returns a decorator function. That returned function then wraps the target:
# What Python actually does:
_decorator = retry(max_attempts=3)  # factory call
fetch_data = _decorator(fetch_data) # real decorator wraps func

# The factory pattern:
def retry(max_attempts=3):
    def decorator(func):         # <-- this is the real decorator
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            # retry logic using max_attempts
            return func(*args, **kwargs)
        return wrapper
    return decorator              # <-- factory returns it
Not quite. The function below is not passed to retry itself. retry(max_attempts=3) is a call with a keyword argument, so Python calls retry first to get back a decorator. That decorator then receives the function:
# Two-step process:
# Step 1: retry(max_attempts=3) -> returns `decorator`
# Step 2: decorator(func) -> returns `wrapper`

# NOT: retry(func, max_attempts=3)
# The parentheses after @retry trigger a call BEFORE
# the function is passed in.
Incorrect. There is no deferred application. The @ expression is evaluated eagerly at definition time. retry(max_attempts=3) runs immediately and returns a function. That function is then used as the decorator:
# Everything happens at definition time, not later:
@retry(max_attempts=3)  # evaluated NOW, returns decorator
def fetch_data(url):    # decorator(fetch_data) called NOW
    pass

# By the time this line runs, fetch_data is already
# the fully-wrapped function.

Practical Stacking Patterns

In production backend code, decorators are frequently stacked to layer cross-cutting concerns onto a single function. The order matters because it determines which checks happen first and which concerns can short-circuit the rest. Here is a common production pattern with the reasoning behind the order:

@classmethod          # 1. outermost: binds to class (must be first)
@log_calls            # 2. logs every call attempt, including failures
@require_role("admin")# 3. rejects unauthorized users before work begins
@cache_with_ttl(60)   # 4. returns cached results, skipping retry/function
@retry(max_attempts=3)# 5. innermost custom: retries on transient failure
def create_report(cls, report_type):
    """Generate and return a business report."""
    pass

This order is intentional. @classmethod goes on the outside because it is a descriptor that must wrap a callable. @log_calls runs on every call, including retries and cache hits, so it sees all activity. @require_role runs before the cache check because unauthorized users should not receive cached data. @cache_with_ttl runs before the retry logic because a cache hit should not trigger retries. @retry is innermost (closest to the function) because it should only fire when the function itself fails, not when auth or caching short-circuits the call.

Reversing any of these layers changes behavior. If @cache_with_ttl were placed outside @require_role, a cached result from an admin's previous call could be served to an unauthorized viewer. If @retry were placed outside @log_calls, the logging decorator would only see the final attempt, missing all the retries that happened before it. If @log_calls were placed inside @retry, each retry attempt would be logged individually—which might be desirable in some contexts, but changes the semantics of what "one call" means in the log output.

The general principle is: decorators that observe or filter should sit outside decorators that transform or retry. Observation decorators (logging, timing, metrics) want to see everything. Filtering decorators (auth, validation) want to reject early, before expensive work happens. Transformation decorators (caching, retry) modify the call path and should operate closest to the function where the actual work happens. This concern-priority ordering produces a stack that is predictable, auditable, and easy to reason about.

Pro Tip

Read a decorator stack top to bottom as "when called, do this first, then this, then this." Read it bottom to top as "this wraps first, then this wraps the result." These two mental models cover every stacking scenario you will encounter.

Python Pop Quiz
You place @cache outside @require_auth. An admin calls the function and the result is cached. What happens when an unauthorized user calls the same function?
Incorrect. Because @cache is outside (above) @require_auth, the cache decorator's wrapper executes first. If there is a cache hit, it returns immediately without ever calling @require_auth's wrapper. Auth is completely bypassed:
# DANGEROUS order:
@cache               # executes 1st -- returns cached result
@require_auth("admin")# never reached on cache hit!
def get_secrets():
    return load_secrets()

# SAFE order:
@require_auth("admin")# executes 1st -- rejects unauthorized
@cache                # only reached if auth passes
def get_secrets():
    return load_secrets()
Correct. This is a real security vulnerability. Outermost decorators execute first. If @cache is outermost, it checks its cache before auth ever runs. A cache hit returns the stored result to anyone, completely bypassing @require_auth:
# Execution flow with @cache outermost:
# 1. cache_wrapper("get_secrets") -> cache HIT
# 2. returns cached result immediately
# 3. require_auth's wrapper NEVER RUNS
# 4. unauthorized user gets admin's data

# Fix: put auth OUTSIDE cache:
@require_auth("admin")  # runs first, rejects unauthorized
@cache                   # only caches for authorized users
def get_secrets():
    return load_secrets()
No, caches do not self-invalidate based on auth status. The cache decorator has no awareness of @require_auth at all. It simply checks if the result exists and returns it. Since it is outermost, it fires before auth and serves the cached result to everyone:
# Each decorator is independent -- cache knows nothing about auth.
# The only thing controlling execution order is stacking position.
# Outermost runs first. If it short-circuits, inner layers are skipped.

Key Takeaways

  1. Application order is bottom to top; execution order is top to bottom: @A @B def f() is f = A(B(f)). B wraps first, but A's wrapper executes first when f() is called.
  2. Always place @classmethod and @staticmethod on the outside: These are descriptors, not callables. A custom decorator that wraps a classmethod object will crash with TypeError: 'classmethod' object is not callable because it bypasses the descriptor protocol. Place them outermost so they wrap a normal callable wrapper function.
  3. Every decorator in the stack needs @functools.wraps(func): Metadata propagates through the chain because each layer copies __name__, __doc__, __module__, __qualname__, __annotations__, and __type_params__ from the previous layer's already-corrected attributes, and sets __wrapped__ to point back. If any layer omits @wraps, the chain breaks and all layers above it inherit the wrong name and docstring.
  4. __wrapped__ forms a linked list through the stack: Each decorator's wrapper points to the function it received. inspect.unwrap() follows this chain to the innermost original function. A broken @wraps in any layer terminates the chain at that point.
  5. Stack order encodes execution priority: Outermost decorators fire first and can short-circuit the rest. Place observability (logging, timing) outermost, security (auth, validation) next, optimization (caching) after that, and resilience (retry) innermost, closest to the function.

Decorator stacking is function composition. The order you choose determines what wraps what, what executes first, and what can prevent the rest from running. Getting this order right is especially critical when descriptors like @classmethod are involved, because the descriptor protocol imposes constraints that standard function-based decorators do not. Place descriptors outermost, use @functools.wraps at every layer, and read the stack top to bottom as the execution sequence. That mental model handles every stacking scenario.

Frequently Asked Questions

In what order does Python apply stacked decorators?

Python applies stacked decorators from bottom to top. The decorator closest to the def statement wraps the function first. Each subsequent decorator wraps the result of the one below it. Writing @A then @B then def func() is equivalent to func = A(B(func)).

In what order do stacked decorators execute when the function is called?

Execution flows from top to bottom (outermost to innermost). The outermost decorator's wrapper runs first, then calls the next layer inward, and so on until the original function executes. Return values flow back outward from innermost to outermost.

Why does putting a custom decorator above @classmethod cause a TypeError?

A classmethod object is a descriptor, not a callable. It only becomes callable when the descriptor protocol calls its __get__ method during attribute access. When a custom decorator wraps the classmethod object directly, it tries to call it and fails because the object has no __call__ method. The fix is to always place @classmethod or @staticmethod as the outermost decorator.

What is the correct order for @classmethod and custom decorators?

Place @classmethod (or @staticmethod) on the outside, above your custom decorators. This means @classmethod wraps your decorator's wrapper function, which is a normal callable. PEP 318 established this convention when decorator syntax was introduced in Python 2.4. For a detailed comparison of when to use each, see classmethod vs staticmethod.

Does functools.wraps propagate correctly through stacked decorators?

Yes, if every decorator in the stack uses @functools.wraps(func). Each layer copies the metadata from whatever it receives. The __wrapped__ attributes form a chain that inspect.unwrap() follows to the innermost original function. If any decorator omits @wraps, the chain breaks at that point. For a full walkthrough, see how to use functools.wraps to preserve metadata in complex stacks.

How do you debug a broken stacked decorator chain?

Walk the __wrapped__ chain at runtime. Each decorator that uses @functools.wraps sets a __wrapped__ attribute pointing to the function it received. Write a loop that follows __wrapped__ at each layer, printing __qualname__ to see exactly which wrappers are in the chain and where it breaks. inspect.unwrap() automates this traversal and returns the innermost original function.

Do parameterized decorators change the stacking rules?

No. A parameterized decorator like @retry(max_attempts=3) is a factory function that returns the real decorator. Python evaluates the factory call first, which produces a standard decorator. That decorator then participates in the normal bottom-to-top stacking process. The @functools.wraps goes inside the innermost wrapper as usual, and the __wrapped__ chain works identically.

Sources

  1. Python Language Reference, Section 8.7: Function Definitions — defines decorator nesting as equivalent to chained function calls.
  2. functools.wraps — Python Standard Library — “This function automatically adds a __wrapped__ attribute to the wrapper.”
  3. inspect.unwrap — Python Standard Library — “It follows the chain of __wrapped__ attributes.”
  4. PEP 318: Decorators for Functions and Methods — introduced decorator syntax in Python 2.4.
  5. abc.abstractmethod — Python Standard Library — documents the required @classmethod above @abstractmethod ordering.
  6. Python Data Model: Implementing Descriptors — defines the __get__, __set__, and __delete__ protocol that @classmethod relies on.