How to Use Class Decorators in Python

Python class decorators come in two kinds: a function placed above a class statement that receives the class and returns it (often changed), and a class whose instances wrap functions through __call__. Here’s the first kind in four lines:

def register(cls):
    REGISTRY[cls.__name__] = cls
    return cls

I tested each decorator here on Python 3.12.5, and the screenshots are straight from Command Prompt.

I’ll cover registering classes, adding attributes and methods, a singleton, the built-in @dataclass and @total_ordering, classes used as decorators, and the order stacked decorators run in.

What is a class decorator in Python?

@register above class Csv is shorthand for Csv = register(Csv). It runs once, right after the class is created, so it’s a natural place to record every class in a plugin registry:

EXPORTERS = {}

def register(cls):
    EXPORTERS[cls.__name__.lower()] = cls
    return cls                      # hand the same class back

@register
class Csv:
    def export(self, total):
        return f"csv,{total:.2f}"

@register
class Json:
    def export(self, total):
        return f'{{"total": {total:.2f}}}'

print(list(EXPORTERS))
for name, cls in EXPORTERS.items():
    print(name, "->", cls().export(1249.5))

Output:

['csv', 'json']
csv -> csv,1249.50
json -> {"total": 1249.50}
Command Prompt output of a Python class decorator registering two exporter classes in a dictionary
Both classes land in the registry the moment they’re defined, and each one is still usable as a normal class.

Before Python 3, jobs like this needed a metaclass. A decorator is simpler, though it doesn’t reach subclasses; my Python metaclasses tutorial explains when you still need one.

Add attributes and methods with a class decorator

A decorator can also change the class it gets. To pass settings, wrap it in one more function, so @tag("sales") first returns the real decorator:

def tag(team, region="US"):
    def decorate(cls):
        cls.team = team
        cls.region = region
        cls.__repr__ = lambda self: f"{cls.__name__}({vars(self)})"
        return cls
    return decorate

@tag("sales", region="TX")
class Order:
    def __init__(self, customer, amount):
        self.customer = customer
        self.amount = amount

o = Order("Emily", 89.99)
print(o)
print(Order.team, Order.region)

Output:

Order({'customer': 'Emily', 'amount': 89.99})
sales TX

The decorator set two class attributes and replaced __repr__. Doing this by name at runtime is covered in dynamic attributes with setattr() and getattr().

Build a singleton with a class decorator

A decorator doesn’t have to return a class. This one returns a function that always hands back the first instance, which is a common way to share one settings object:

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

@singleton
class Settings:
    def __init__(self, currency="USD"):
        self.currency = currency

a = Settings()
b = Settings(currency="EUR")       # ignored: the first object is reused
print(a is b, b.currency)
print(type(Settings).__name__)
try:
    isinstance(a, Settings)
except TypeError as err:
    print("TypeError:", err)

Output:

True USD
function
TypeError: isinstance() arg 2 must be a type, a tuple of types, or a union
Command Prompt output of a Python singleton class decorator returning the same object and isinstance raising TypeError
The second call reuses the first object, but Settings is now a function, so isinstance() fails.

That TypeError is the catch: the name no longer points to a class. If you need isinstance() to work, put the logic in __new__ instead; see __new__ vs __init__ in Python.

Built-in class decorators: @dataclass and @total_ordering

The standard library ships class decorators you probably already use. @dataclass writes __init__, __repr__ and __eq__ for you, and @total_ordering fills in the missing comparison methods:

from dataclasses import dataclass
from functools import total_ordering

@dataclass
class Invoice:
    number: str
    total: float

print(Invoice("INV-1001", 250.0))
print(Invoice("INV-1001", 250.0) == Invoice("INV-1001", 250.0))

@total_ordering
class Version:
    def __init__(self, major, minor):
        self.key = (major, minor)
    def __eq__(self, other):
        return self.key == other.key
    def __lt__(self, other):
        return self.key < other.key

print(Version(3, 12) >= Version(3, 9), Version(2, 7) > Version(3, 0))

Output:

Invoice(number='INV-1001', total=250.0)
True
True False
Command Prompt output of Python @dataclass generating repr and equality and @total_ordering adding comparison operators
The dataclass prints its fields and compares by value; Version supports >= and > though it only defines two methods.

For defaults, ordering and frozen objects, my Python dataclass guide goes further.

How do you use a class as a decorator?

Flip it around and a class can decorate a function. __init__ receives the function and __call__ runs on every call, so the instance can keep state such as a call count:

import functools

class CountCalls:
    def __init__(self, func):
        functools.update_wrapper(self, func)   # copy __name__, __doc__
        self.func = func
        self.calls = 0

    def __call__(self, *args, **kwargs):
        self.calls += 1
        print(f"call {self.calls} of {self.__name__}")
        return self.func(*args, **kwargs)

@CountCalls
def sales_tax(amount, rate=0.0825):
    """Texas sales tax."""
    return round(amount * rate, 2)

print(sales_tax(100))
print(sales_tax(59.99))
print(sales_tax.__name__, "|", sales_tax.__doc__, "|", sales_tax.calls)

Output:

call 1 of sales_tax
8.25
call 2 of sales_tax
4.95
sales_tax | Texas sales tax. | 2
Command Prompt output of a Python class used as a decorator with __call__ counting calls and keeping the function name
Each call prints its number, and functools.update_wrapper() keeps the original name and docstring.

Without update_wrapper(), sales_tax.__name__ raises AttributeError, because instances don’t have one. Objects with __call__ count as callable; see the callable() function in Python.

In what order do stacked class decorators run?

Decorators apply from the bottom up: the one closest to class runs first, and its result goes to the one above:

def mark(label):
    def decorate(cls):
        print("applying", label)
        cls.labels = getattr(cls, "labels", []) + [label]
        return cls
    return decorate

@mark("outer")
@mark("inner")
class Report:
    pass

print(Report.labels)

Output:

applying inner
applying outer
['inner', 'outer']

So @mark("outer") @mark("inner") means mark("outer")(mark("inner")(Report)). Method decorators like the @property decorator follow the same rule.

You might like these class guides next:

Frequently asked questions

What is a class decorator in Python?

A function that takes a class and returns a class (or a replacement), applied with @name above the class statement.

Do class decorators affect subclasses?

No. The decorator runs once on the decorated class; subclasses inherit what it added but aren’t decorated again.

How do I pass arguments to a class decorator?

Write a function that takes the arguments and returns the real decorator, like @tag("sales").

Why does isinstance() fail after a singleton decorator?

The decorator returned a function, so the name isn’t a class anymore. Use __new__ for the singleton instead.

What does functools.wraps do in a class-based decorator?

Use functools.update_wrapper(self, func) in __init__ to copy the name and docstring onto the instance.

Where are update_wrapper and total_ordering documented?

See the functools module in the Python docs.