Class Decorators: Modifying Classes at Definition Time
A class decorator is a function that takes a class as input and returns a modified (or wrapped) version of that class. Unlike function decorators, class decorators run at class definition time, giving you a moment to inspect, modify, or register the class before any instances are created. Class decorators are simpler and more readable than metaclasses for many tasks: adding debug logging, auto-registering plugins, converting methods to properties, or enforcing naming conventions. Frameworks like FastAPI, Celery, and attrs use class decorators to provide elegant, declarative APIs.
Simple Class Decorator: Adding a Method
The simplest class decorator adds or modifies class methods. Here's a decorator that adds a __repr__ method:
def add_repr(cls):
"""A class decorator that adds a useful __repr__ method."""
def __repr__(self):
attrs = ', '.join(
f'{k}={v!r}'
for k, v in self.__dict__.items()
if not k.startswith('_')
)
return f'{cls.__name__}({attrs})'
cls.__repr__ = __repr__
return cls
@add_repr
class Book:
def __init__(self, title, author):
self.title = title
self.author = author
book = Book("1984", "George Orwell")
print(book) # Output: Book(title='1984', author='George Orwell')
The decorator receives the class Book after its definition, adds a custom __repr__, and returns the modified class. This is cleaner than manually writing __repr__ in every class.
Plugin Registry Decorator
A powerful pattern is auto-registering subclasses into a registry at definition time:
class PluginRegistry:
"""Maintains a registry of plugin classes."""
_plugins = {}
@classmethod
def register(cls, plugin_cls):
"""Class decorator to register a plugin."""
name = plugin_cls.__name__
if name in cls._plugins:
raise ValueError(f'Plugin {name} already registered')
cls._plugins[name] = plugin_cls
return plugin_cls
@classmethod
def get(cls, name):
"""Retrieve a plugin by name."""
return cls._plugins.get(name)
@classmethod
def list_all(cls):
"""List all registered plugins."""
return list(cls._plugins.keys())
@PluginRegistry.register
class JSONExporter:
def export(self, data):
import json
return json.dumps(data)
@PluginRegistry.register
class CSVExporter:
def export(self, data):
import csv
import io
output = io.StringIO()
writer = csv.DictWriter(output, fieldnames=data[0].keys())
writer.writeheader()
writer.writerows(data)
return output.getvalue()
# Usage
print(PluginRegistry.list_all()) # Output: ['JSONExporter', 'CSVExporter']
exporter = PluginRegistry.get('JSONExporter')()
print(exporter.export([{'id': 1, 'name': 'Alice'}]))
This pattern is used in web frameworks (Flask blueprints), task queues (Celery tasks), and plugin systems. Classes register themselves at import time without explicit registration code.
Decorator with Arguments: Parameterized Transformation
Decorators can take arguments to customize behavior. The syntax is a decorator factory returning a decorator:
def validates_type(*type_fields):
"""
A parameterized class decorator that auto-generates type-checking setters
for specified fields.
"""
def decorator(cls):
for field_name, field_type in type_fields:
# Create a property that validates type on assignment
private_attr = f'_{field_name}'
def make_property(fname, ftype):
def getter(self):
return getattr(self, private_attr, None)
def setter(self, value):
if not isinstance(value, ftype):
raise TypeError(
f'{fname} must be {ftype.__name__}, '
f'got {type(value).__name__}'
)
setattr(self, private_attr, value)
return property(getter, setter)
setattr(cls, field_name, make_property(field_name, field_type))
return cls
return decorator
@validates_type(('name', str), ('age', int), ('email', str))
class User:
def __init__(self, name, age, email):
self.name = name
self.age = age
self.email = email
user = User("Alice", 30, "[email protected]")
print(user.name) # Output: Alice
# Type error
try:
user.age = "thirty"
except TypeError as e:
print(f'Error: {e}')
The @validates_type decorator accepts a list of (field, type) tuples and dynamically creates properties for each. Parameterized decorators are more flexible than simple decorators and are the building block of frameworks.
Decorator Composition: Stacking Transformations
Multiple decorators can be combined, each adding behavior:
def add_json_serialization(cls):
"""Add to_dict and from_dict methods."""
def to_dict(self):
return self.__dict__.copy()
def from_dict(cls, data):
return cls(**data)
cls.to_dict = to_dict
cls.from_dict = classmethod(from_dict)
return cls
def add_debug_logging(cls):
"""Log __init__ calls."""
original_init = cls.__init__
def logged_init(self, *args, **kwargs):
print(f'Creating {cls.__name__} with args={args}, kwargs={kwargs}')
original_init(self, *args, **kwargs)
cls.__init__ = logged_init
return cls
@add_json_serialization
@add_debug_logging
class Product:
def __init__(self, name, price):
self.name = name
self.price = price
product = Product("Widget", 19.99)
# Output: Creating Product with args=(), kwargs={'name': 'Widget', 'price': 19.99}
print(product.to_dict()) # Output: {'name': 'Widget', 'price': 19.99}
Decorators are applied bottom-to-top, so add_debug_logging runs first, then add_json_serialization. This allows composable, reusable transformations.
Class Decorator vs. Metaclass vs. Inheritance
| Approach | When to Use | Complexity |
|---|---|---|
| Class decorator | Add methods, register classes, simple transformations | Low |
| Inheritance | Share behavior across a hierarchy | Low |
| Metaclass | Control class creation, enforce constraints, modify multiple aspects | High |
Key Takeaways
- A class decorator is a function taking a class and returning a (usually modified) class, applied with
@decoratorsyntax. - Class decorators run at class definition time, making them ideal for registration, method injection, and one-time transformations.
- Parameterized decorators use a factory pattern:
@decorator_factory(args)returns a decorator function. - Multiple decorators compose by stacking; each receives the result of the previous one.
Frequently Asked Questions
What is the difference between a class decorator and a metaclass?
A class decorator is simpler and more readable; it's a regular function applied at class definition. A metaclass is a class whose instances are classes; it controls the class creation process itself. Use decorators for simple transformations; use metaclasses when you need to enforce behavior across a complex hierarchy or control how subclasses are created.
Do class decorators work with inheritance?
Yes. If you decorate a parent class, subclasses inherit the decorated behavior. If you decorate a subclass, only that subclass is modified. Decorators applied to both parent and child run independently.
Can I undo a class decorator?
Not automatically. A decorator modifies the class in place (or returns a new class). To reverse changes, you'd need to store the original and restore it explicitly. This is rarely needed; if you need conditional behavior, use if statements inside the decorator.
How do class decorators interact with slots?
Class decorators can modify a class using __slots__. However, if you try to add new instance attributes via the decorator, they won't work with __slots__ unless you add them to the __slots__ tuple itself in the decorator.
Further Reading
- Python Class Decorators — official definition and documentation.
- Functools wraps for Class Decorators — preserving metadata when wrapping.
- FastAPI Dependency Injection — real-world class decorator usage in web frameworks.
- Attrs: Class Decorators Done Right — production-grade library using decorators.