Class Decorators
Deep dive · part of Python Decorators
A decorator can target a class instead of a function. It receives the class object, can mutate it (add attributes, wrap methods, register it in a global table) and must return the resulting class. Class decorators are a cleaner, more discoverable alternative to metaclasses for many common use cases.
Class decorators use the same @syntax as function decorators, but they receive a class object and must return a class—usually the same one, possibly modified. They shine for registration tables, automatic __repr__ helpers, and enforcing simple invariants at definition time without importing metaclass machinery.
Because they run when the class statement executes—before any instances exist—they can inspect methods on the class body, attach class-level metadata, or reject invalid designs early. Many teams reach for a class decorator when a metaclass would be heavier than the problem deserves.
A class decorator is any callable taking cls and returning cls (or a replacement); @deco above class Foo is equivalent to Foo = deco(Foo).
Common patterns: plugin registries (REGISTRY[name] = cls), auto-generated __repr__, wrapping every public method for logging or timing.
Class decorators mutate or replace the class namespace; they do not control instance creation—that is metaclass territory.
They compose with inheritance: the decorated class is what subclasses see; order matters if you stack multiple decorators.
Use functools.wraps-style preservation only when your decorator returns a wrapper function, not when returning the class itself.
Prefer explicit registration APIs in large frameworks, but decorators keep tutorial-scale plugins discoverable.
Implementation detail: the decorator runs once per class definition, so expensive work belongs in __init__ or module-level setup unless you truly need compile-time checks. When wrapping methods, iterate vars(cls) and skip names starting with __ unless you intend to intercept magic methods.
Compared to metaclasses, class decorators cannot change how instances are constructed or how attribute lookup works on instances—they only see the finished class object. That limitation is a feature for readability.
In production codebases, document decorated classes in __all__ or plugin manifests so static analysis and IDEs still find implementations after dynamic registration.
Returning None or a non-type from a class decorator, which breaks subclassing silently until import time errors appear.
Wrapping __init__ without functools.wraps on the inner function, losing signature metadata for inspect and help().
Applying decorators that depend on instance state—there is no self at class definition time.
Replacing the class with a function or proxy unless you fully control all call sites.
Keep class decorators pure and side-effect light; push I/O and network calls to instance methods.
Name registration decorators clearly (@register_exporter) and expose the registry for tests.
When wrapping all methods, skip dunder names unless you understand descriptor interaction.
Add a unit test that imports the module and asserts the class landed in your registry.
Re-read the examples below with these ideas in mind; change variable names and inputs to match your own project.
The program below demonstrates register subclasses. Read the comments on each line, run the code, then change names or values to see how the output shifts.
# Example: Register subclasses
# Run in the REPL or save as a .py file and execute with python.
REGISTRY = {}
def register(cls):
REGISTRY[cls.__name__] = cls
return cls
@register
class JsonExporter: ...
@register
class CsvExporter: ...
print(REGISTRY)
This sample walks through auto-add repr in a small, runnable script. Paste it into the REPL or save it as a .py file before you continue to the next block.
# Example: Auto-add repr
# Run in the REPL or save as a .py file and execute with python.
def autorepr(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
@autorepr
class Point:
def __init__(self, x, y):
self.x, self.y = x, y
print(Point(3, 4))
Here is a hands-on illustration of wrap every method. Follow the inline comments first; only then execute the snippet and compare the result with what you expected.
# Example: Wrap every method
# Run in the REPL or save as a .py file and execute with python.
import time, functools
def trace_all(cls):
for name, attr in vars(cls).items():
if callable(attr) and not name.startswith("__"):
@functools.wraps(attr)
def wrapper(*a, _f=attr, **kw):
t0 = time.perf_counter()
r = _f(*a, **kw)
print(f"{_f.__name__} {time.perf_counter()-t0:.5f}s")
return r
setattr(cls, name, wrapper)
return cls
@trace_all
class Calc:
def add(self, a, b): return a + b
def mul(self, a, b): return a * b
c = Calc(); c.add(2, 3); c.mul(4, 5)
The program below demonstrates registry decorator. Read the comments on each line, run the code, then change names or values to see how the output shifts.
# Class decorators receive the class object and return a (possibly) modified class
REGISTRY = {} # global plugin table
def register(cls): # class decorator
REGISTRY[cls.__name__] = cls # store by name
return cls # must return class
@register # apply to class
class CsvExporter: # exporter implementation
ext = "csv" # metadata
@register # second plugin
class JsonExporter: # another exporter
ext = "json" # metadata
print(sorted(REGISTRY)) # registered names
This sample walks through auto repr in a small, runnable script. Paste it into the REPL or save it as a .py file before you continue to the next block.
# Decorators can inject methods onto classes
def autorepr(cls): # add __repr__
def __repr__(self): # instance repr
fields = ", ".join(f"{k}={v!r}" for k, v in vars(self).items()) # attrs
return f"{cls.__name__}({fields})" # className(k=v)
cls.__repr__ = __repr__ # attach
return cls # return class
@autorepr # decorate
class Point: # simple data class
def __init__(self, x, y): # ctor
self.x, self.y = x, y # store
print(Point(3, 4)) # Point(x=3, y=4)
Related deep dives on Python Decorators: