Skip to main content

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

ApproachWhen to UseComplexity
Class decoratorAdd methods, register classes, simple transformationsLow
InheritanceShare behavior across a hierarchyLow
MetaclassControl class creation, enforce constraints, modify multiple aspectsHigh

Key Takeaways

  • A class decorator is a function taking a class and returning a (usually modified) class, applied with @decorator syntax.
  • 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