DEV Community

Cover image for Python by Structure: Decorator Chains and Execution Order
Aaron Rose
Aaron Rose

Posted on

Python by Structure: Decorator Chains and Execution Order

Timothy was debugging a data processing function that wasn't working correctly. "Margaret, I added decorators to validate the data and log the results, but I'm getting validation errors on values that should be fine. Look - the decorators are all here."

Margaret examined his code:

@validate_result
@transform_data
@sanitize_input
def process_user_data(data):
    return calculate_score(data)
Enter fullscreen mode Exit fullscreen mode

"The decorators are there," Margaret said, "but do you understand the order they execute in?"

Timothy blinked. "They... run top to bottom, right? Like the rest of Python?"

"That's what everyone thinks at first. But decorators are different. Let me show you what's really happening."

The Problem: Decorator Order Confusion

"When you stack decorators," Margaret explained, "they execute bottom-to-top, not top-to-bottom. Your sanitize runs first, then transform, then validate. But you're validating the transformed result, not the original input."

She pointed at the structure. "You sanitize the raw input, transform it, then validate the transformed output. But you probably want to validate the raw input before transforming it."

Timothy frowned. "So the bottom decorator wraps the function first, then each decorator above wraps that result?"

"Exactly. Let me show you the structure."

Tree View:

@validate_result
@transform_data
@sanitize_input
process_user_data(data)
    Return calculate_score(data)
Enter fullscreen mode Exit fullscreen mode

English View:

Decorator @validate_result
Decorator @transform_data
Decorator @sanitize_input
Function process_user_data(data):
  Return calculate_score(data).
Enter fullscreen mode Exit fullscreen mode

Timothy studied the structure. "So even though @validate_result appears at the top, it's actually the outermost wrapper - the last thing applied?"

"Precisely," Margaret confirmed. "Think of it like wrapping a gift. The bottom decorator is the first layer of wrapping paper, closest to the function. Each decorator above adds another layer. The top decorator is the outermost layer."

Understanding Decorator Execution

Margaret pulled up a simpler example to make the concept clearer:

@decorator_one
@decorator_two
@decorator_three
def my_function():
    return "Hello"
Enter fullscreen mode Exit fullscreen mode

Tree View:

@decorator_one
@decorator_two
@decorator_three
my_function()
    Return 'Hello'
Enter fullscreen mode Exit fullscreen mode

English View:

Decorator @decorator_one
Decorator @decorator_two
Decorator @decorator_three
Function my_function():
  Return 'Hello'.
Enter fullscreen mode Exit fullscreen mode

"The structure shows them top-to-bottom as written," Margaret said, "but Python applies them bottom-to-top. It's equivalent to:"

my_function = decorator_one(decorator_two(decorator_three(my_function)))
Enter fullscreen mode Exit fullscreen mode

Timothy traced through the logic. "So decorator_three wraps the original function first. Then decorator_two wraps that result. Then decorator_one wraps everything?"

"Exactly. The bottom decorator is closest to the function. Each decorator above wraps the previous result. When you call my_function(), the call goes through decorator_one first, then decorator_two, then decorator_three, then finally the actual function."

She showed him what this looks like with decorators that do something visible:

def first_wrapper(func):
    def wrapper():
        print("First: before")
        result = func()
        print("First: after")
        return result
    return wrapper

def second_wrapper(func):
    def wrapper():
        print("Second: before")
        result = func()
        print("Second: after")
        return result
    return wrapper

@first_wrapper
@second_wrapper
def greet():
    print("Hello!")
    return "done"
Enter fullscreen mode Exit fullscreen mode

"When you call greet(), you'll see:"

First: before
Second: before
Hello!
Second: after
First: after
Enter fullscreen mode Exit fullscreen mode

"See?" Margaret pointed out. "The first decorator's 'before' runs first, but it's the last decorator applied. The execution flows from outermost to innermost going in, and innermost to outermost coming out."

Property Decorators - A Special Case

"What about @property?" Timothy asked. "I see that all the time in classes."

Margaret showed him a temperature converter:

class Temperature:
    def __init__(self):
        self._celsius = 0

    @property
    def celsius(self):
        return self._celsius

    @celsius.setter
    def celsius(self, value):
        self._celsius = value
Enter fullscreen mode Exit fullscreen mode

Tree View:

class Temperature
    __init__(self)
        self._celsius = 0
    @property
    celsius(self)
        Return self._celsius
    @celsius.setter
    celsius(self, value)
        self._celsius = value
Enter fullscreen mode Exit fullscreen mode

English View:

Class Temperature:
  Function __init__(self):
    Set self._celsius to 0.
  Decorator @property
  Function celsius(self):
    Return self._celsius.
  Decorator @celsius.setter
  Function celsius(self, value):
    Set self._celsius to value.
Enter fullscreen mode Exit fullscreen mode

"Look at the structure," Margaret said. "The @property decorator turns the celsius method into a getter. Then @celsius.setter adds a setter to the same property. The order matters here - you must define the property getter first with @property, then you can add the setter with @celsius.setter."

Timothy nodded. "So @property creates the property object, and then @celsius.setter modifies that same object to add the setter behavior?"

"Exactly. This is why you can't do @celsius.setter before @property - the property object doesn't exist yet."

Getting the Order Right

Timothy looked back at his data processing function. "So I need to think about what should happen first - closest to the function - and work outward?"

"Exactly," Margaret said. "Let's fix your function. What do you want to happen in what order?"

Timothy thought about it. "I want to validate the raw input first, then sanitize it if it passes validation, then transform it, then log the final result."

"Perfect. Now let's structure that bottom-to-top:"

@log_result        # Outermost - logs the final transformed result
@transform_data    # Middle - transforms the sanitized data
@sanitize_input    # Second - cleans the validated input
@validate_input    # Innermost - validates raw input first
def process_user_data(data):
    return calculate_score(data)
Enter fullscreen mode Exit fullscreen mode

Tree View:

@log_result
@transform_data
@sanitize_input
@validate_input
process_user_data(data)
    Return calculate_score(data)
Enter fullscreen mode Exit fullscreen mode

English View:

Decorator @log_result
Decorator @transform_data
Decorator @sanitize_input
Decorator @validate_input
Function process_user_data(data):
  Return calculate_score(data).
Enter fullscreen mode Exit fullscreen mode

"Now trace the execution," Margaret said. "When someone calls process_user_data(data), the call enters through log_result first - the outermost wrapper. Then it goes through transform_data, then sanitize_input, then validate_input, and finally reaches your function."

Timothy traced it through. "So the flow is: log wrapper starts → transform wrapper starts → sanitize wrapper starts → validate wrapper starts → validate runs → sanitize runs → transform runs → my function runs → transform finishes → sanitize finishes → validate finishes → log finishes."

"Exactly. On the way in, you go through each wrapper's 'before' code from outermost to innermost. Then your function executes. Then on the way out, you go through each wrapper's 'after' code from innermost to outermost."

Decorators with Arguments

"I've also seen decorators that take arguments," Timothy said. "How does that work with the order?"

Margaret showed him:

def repeat(times):
    def decorator(func):
        def wrapper(*args, **kwargs):
            for _ in range(times):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

@repeat(3)
@log_call
def greet(name):
    print(f"Hello, {name}!")
Enter fullscreen mode Exit fullscreen mode

Tree View:

@repeat(3)
@log_call
greet(name)
    print(f'Hello, {name}!')
Enter fullscreen mode Exit fullscreen mode

English View:

Decorator @repeat(3)
Decorator @log_call
Function greet(name):
  Evaluate print(f'Hello, {name}!').
Enter fullscreen mode Exit fullscreen mode

"When a decorator takes arguments," Margaret explained, "it's actually a decorator factory. The repeat(3) call returns a decorator, which then gets applied to your function. But the bottom-to-top rule still applies."

"So this is equivalent to:"

greet = repeat(3)(log_call(greet))
Enter fullscreen mode Exit fullscreen mode

Timothy worked through it. "The log_call decorator wraps the function first - that's the innermost wrapper. Then the decorator returned by repeat(3) wraps that result."

"Right. When you call greet('Alice'), it goes through the repeat wrapper first, which calls the log wrapper three times, which calls your function three times."

Class Decorators

"Can you decorate classes too?" Timothy asked.

"Absolutely," Margaret said. She showed him a dataclass example:

from dataclasses import dataclass

@dataclass
class Point:
    x: float
    y: float
Enter fullscreen mode Exit fullscreen mode

Tree View:

@dataclass
class Point
    x: float = …
    y: float = …
Enter fullscreen mode Exit fullscreen mode

English View:

Decorator @dataclass
Class Point:
  Declare x: float = ….
  Declare y: float = ….
Enter fullscreen mode Exit fullscreen mode

"The @dataclass decorator transforms the class, adding __init__, __repr__, __eq__, and other methods automatically. If you had multiple class decorators, they'd follow the same bottom-to-top application rule."

Timothy refactored his data processing function, carefully ordering the decorators from innermost (what happens first, closest to the function) to outermost (what happens last, wrapping everything else).

"The structure shows them as written," Timothy said, "but I need to remember they execute bottom-to-top. The bottom decorator is the first wrapper, the top decorator is the last wrapper."

"Now you understand decorator chains," Margaret said. "The visual stack in the code is deceptive - execution flows from the bottom up when applying decorators, and from the top down when calling the decorated function."


Explore Python structure yourself: Download the Python Structure Viewer - a free tool that shows code structure in tree and plain English views. Works offline, no installation required.


Aaron Rose is a software engineer and technology writer at tech-reader.blog and the author of Think Like a Genius.

Top comments (0)