Python Class Decorators: Construction and Evaluation

Class decorators in Python provide a clean, declarative syntax for modifying or enhancing class definitions dynamically. This article explores how Python constructs class decorators under the hood, how they alter or replace class objects, and the precise timing of their evaluation during a program's lifecycle.

What Is a Class Decorator?

A class decorator is a callable—typically a function or another class—that accepts a class object as its sole argument and returns either the modified class, an entirely new class, or a wrapper object.

Syntactically, applying a class decorator looks like this:

@my_decorator
class MyClass:
    pass

Behind the scenes, the @ syntax is syntactic sugar. Python translates the code above into an explicit function call immediately after the class body executes:

class MyClass:
    pass

MyClass = my_decorator(MyClass)

How Python Constructs Class Decorators

To understand how decorators operate on classes, it helps to understand how Python builds a class in the first place:

  1. Body Execution: Python executes the class body as a code block within a dedicated namespace dictionary.
  2. Class Creation: Python invokes the metaclass (by default, type) using the class name, base classes, and the populated namespace to construct the class object in memory.
  3. Decorator Application: Once the class object is instantiated, Python immediately passes that class object into the decorator callable.
  4. Rebinding: The original identifier (MyClass) is rebound to whatever object the decorator returns.

Implementing a Basic Class Decorator

A decorator can mutate the class directly and return it:

def add_greeting(cls):
    cls.greeting = "Hello, World!"
    return cls

@add_greeting
class User:
    pass

print(User.greeting)  # Outputs: Hello, World!

Alternatively, a decorator can wrap the original class in an entirely new class or proxy, intercepting instantiation or method calls:

def singleton(cls):
    instances = {}
    def get_instance(*args, **kwargs):
        if cls not in instances:
            instances[cls] = cls(*args, **kwargs)
        return instances[cls]
    return get_instance

@singleton
class DatabaseConnection:
    pass

In this case, DatabaseConnection no longer points to the class itself, but to the get_instance closure.

When Are Class Decorators Evaluated?

Class decorators are evaluated at definition time (often referred to as import time), not at runtime when instances of the class are created.

When Python imports a module or encounters a class block during execution:

  1. The class body runs from top to bottom.
  2. The class object is constructed.
  3. The decorator function runs immediately.

Demonstrating Evaluation Timing

Consider this example showing the exact execution order:

def track_decorator(cls):
    print("Decorator function evaluated.")
    return cls

print("Before class definition.")

@track_decorator
class Order:
    print("Inside class body.")

print("After class definition.")

# Creating instances later
order1 = Order()
order2 = Order()

Output:

Before class definition.
Inside class body.
Decorator function evaluated.
After class definition.

Notice that track_decorator runs before any Order instance is created and before the code following the class definition executes. Instantiating Order() does not re-trigger the decorator.

Decorators with Arguments

When a decorator takes arguments, it requires an extra layer of construction known as a decorator factory.

def set_tag(tag_name):
    print(f"1. Factory called with tag: {tag_name}")
    def decorator(cls):
        print(f"2. Decorator applied to {cls.__name__}")
        cls.tag = tag_name
        return cls
    return decorator

@set_tag("admin")
class Account:
    pass

Evaluation Order with Arguments:

  1. Python encounters @set_tag("admin") and evaluates set_tag("admin") first, producing the actual decorator function.
  2. Python executes the body of Account and creates the class object.
  3. Python calls the resulting decorator with Account as the argument.
  4. Account is rebound to the decorator's return value.