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)
- Syntax and mechanics
- Simple enrichment: adding methods and attributes
- Decorators with arguments
- Stacking class decorators
dataclassis a class decorator- Registration decorator (plugin pattern)
- Enforcing interfaces (lightweight)
- Wrapping classes — rare but possible
- Class decorator vs metaclass
- Preserving metadata with
functools.wraps— classes? - Real-world:
total_orderingand friends - 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 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#
- When does a class decorator run relative to the class body and metaclass
__new__? - How do parameterized class decorators use the factory pattern?
- Why does
@alpha @beta class Crunbetabeforealpha? - When should you choose a metaclass over a class decorator?
- How does
@dataclassillustrate 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 |