Skip to content

Class Decorators#

A class decorator is a callable that receives a class object and returns a class (usually the same one, modified, or a wrapper). Class decorators run after the class body finishes — unlike metaclasses, which participate in class construction.

How to use this page

Read after Decorators and Classes and OOP Basics. Compare with Metaclasses when you need pre-creation hooks. For function-level wrapping, see Callable Objects.

At a glance
Track Python Advanced → Meta Programming
Sections 12 major topics
Outline Use the right-hand TOC to jump

Topics: Syntax and mechanics · Simple enrichment: adding methods and attributes · Decorators with arguments · Stacking class decorators · dataclass is a class decorator · Registration decorator (plugin pattern) · Enforcing interfaces (lightweight) · Wrapping classes — rare but possible · … (+4 more)

  1. Syntax and mechanics
  2. Simple enrichment: adding methods and attributes
  3. Decorators with arguments
  4. Stacking class decorators
  5. dataclass is a class decorator
  6. Registration decorator (plugin pattern)
  7. Enforcing interfaces (lightweight)
  8. Wrapping classes — rare but possible
  9. Class decorator vs metaclass
  10. Preserving metadata with functools.wraps — classes?
  11. Real-world: total_ordering and friends
  12. Combining with function decorators

Syntax and mechanics#

def debug(cls):
    print(f"Defined: {cls.__name__}")
    return cls

@debug
class User:
    pass
# stdout: Defined: User

Desugaring:

class User:
    pass

User = debug(User)

Class decorators use the same @ syntax as function decorators but operate on class objects.


Simple enrichment: adding methods and attributes#

def add_repr(cls):
    def __repr__(self):
        fields = ", ".join(f"{k}={v!r}" for k, v in vars(self).items())
        return f"{cls.__name__}({fields})"
    cls.__repr__ = __repr__
    return cls

@add_repr
class Point:
    def __init__(self, x: int, y: int):
        self.x, self.y = x, y

Point(1, 2)  # Point(x=1, y=2)
Pattern Example
Add method cls.method = ...
Add class attribute cls.VERSION = "1.0"
Replace dunder cls.__repr__ = ...
Register externally registry[cls.__name__] = cls

Decorators with arguments#

Use a factory that returns the actual decorator:

def serialize(*fields: str):
    def decorator(cls):
        def to_dict(self) -> dict:
            return {f: getattr(self, f) for f in fields}
        cls.to_dict = to_dict
        return cls
    return decorator

@serialize("id", "name")
class User:
    def __init__(self, id: int, name: str):
        self.id, self.name = id, name

User(1, "Ada").to_dict()  # {'id': 1, 'name': 'Ada'}

Same two-level pattern as parameterized function decorators in Decorators.


Stacking class decorators#

Applied bottom-up (innermost @ closest to class runs first):

def alpha(cls):
    cls.order = getattr(cls, "order", []) + ["alpha"]
    return cls

def beta(cls):
    cls.order = getattr(cls, "order", []) + ["beta"]
    return cls

@alpha
@beta
class Combined:
    pass

Combined.order  # ['beta', 'alpha']

dataclass is a class decorator#

from dataclasses import dataclass, field

@dataclass(frozen=True, order=True)
class Point:
    x: int
    y: int
    tags: list[str] = field(default_factory=list)

The stdlib @dataclass generates __init__, __repr__, __eq__, and optional ordering — the canonical example of class decoration over manual boilerplate.


Registration decorator (plugin pattern)#

PLUGINS: dict[str, type] = {}

def register(name: str):
    def decorator(cls):
        PLUGINS[name] = cls
        return cls
    return decorator

@register("csv")
class CsvExporter:
    def export(self, data): ...

@register("json")
class JsonExporter:
    def export(self, data): ...

PLUGINS["csv"]  # <class 'CsvExporter'>

Alternative: metaclass registry in Metaclasses. Decorators are more explicit and easier to grep.


Enforcing interfaces (lightweight)#

def requires(*methods: str):
    def decorator(cls):
        for method in methods:
            if not hasattr(cls, method):
                raise TypeError(f"{cls.__name__} missing {method}")
        return cls
    return decorator

@requires("save", "load")
class Repository:
    def save(self): ...
    def load(self): ...

For rigorous abstract interfaces, use Abstract Base Classes. Class decorators suit quick project-local guards.


Wrapping classes — rare but possible#

Return a new class or proxy instead of mutating in place:

def singleton(cls):
    instances: dict[type, object] = {}

    class Wrapper:
        def __call__(self, *args, **kwargs):
            if cls not in instances:
                instances[cls] = cls(*args, **kwargs)
            return instances[cls]

        def __getattr__(self, name):
            return getattr(cls, name)

    return Wrapper()

@singleton
class Database:
    def __init__(self, url: str):
        self.url = url

Typing and isinstance break on wrappers

isinstance(Database(), Database) may fail if Database is rebound to a wrapper. Prefer metaclass __call__ or module-level instance for singletons.


Class decorator vs metaclass#

Criterion Class decorator Metaclass
Runs After class created During class creation
Mutate namespace before class exists No Yes (__new__)
Control Cls() instance creation No (unless wrapper) Yes (__call__)
Readability Higher Lower
Framework gravity App-level enrichment ORM / DSL frameworks

Rule: default to class decorators; escalate to metaclass only when decoration is too late.


Preserving metadata with functools.wraps — classes?#

wraps targets functions. For classes, manually copy __name__, __qualname__, __module__, __doc__:

def preserve_meta(decorator):
    def wrap(cls):
        result = decorator(cls)
        result.__name__ = cls.__name__
        result.__qualname__ = cls.__qualname__
        result.__doc__ = cls.__doc__
        return result
    return wrap

Or avoid replacing the class object — mutate in place and return cls.


Real-world: total_ordering and friends#

from functools import total_ordering

@total_ordering
class Version:
    def __init__(self, major: int, minor: int):
        self.major, self.minor = major, minor

    def __eq__(self, other):
        return (self.major, self.minor) == (other.major, other.minor)

    def __lt__(self, other):
        return (self.major, self.minor) < (other.major, other.minor)

Version(1, 0) < Version(2, 0)  # True

Stdlib class decorators are excellent templates for your own.


Combining with function decorators#

def logged_method(func):
    def wrapper(*args, **kwargs):
        print(f"calling {func.__name__}")
        return func(*args, **kwargs)
    return wrapper

def log_all_methods(cls):
    for name, attr in cls.__dict__.items():
        if callable(attr) and not name.startswith("_"):
            setattr(cls, name, logged_method(attr))
    return cls

@log_all_methods
class Service:
    def run(self):
        return "ok"

For per-method control, use function decorators inside the class body instead.


Interview traps (quick reference)#

Trap What goes wrong Safe approach
Decorator returns None Class becomes None Always return cls (or wrapper)
Stacking order confusion Wrong init order Bottom decorator runs first
Replacing class breaks isinstance Type checks fail Mutate in place when possible
Class decorator for __slots__ enforcement Too late — slots fixed at creation Metaclass or base with __slots__
Same name as function decorator Import / apply wrong callable Namespace carefully

Mental model checklist#

  1. When does a class decorator run relative to the class body and metaclass __new__?
  2. How do parameterized class decorators use the factory pattern?
  3. Why does @alpha @beta class C run beta before alpha?
  4. When should you choose a metaclass over a class decorator?
  5. How does @dataclass illustrate the power of class decoration?

What's next#

Topic Page
Function decorators Decorators
Metaclasses Metaclasses
ABCs Abstract Base Classes
Dunder methods Dunder or Magic Methods
Dynamic classes type() for Dynamic Class Creation