Understanding __init_subclass__ for Automatic Registration
__init_subclass__ is a special method that runs automatically when a class is subclassed, without requiring a metaclass or decorator. Introduced in Python 3.6, it allows parent classes to customize how subclasses behave at definition time. Unlike decorators (which you must explicitly apply) or metaclasses (which are complex), __init_subclass__ is a straightforward hook: define it in a base class, and every subclass will trigger it. This makes it ideal for plugin registries, automatic validation, enforcing naming conventions, and building extensible frameworks. Many modern libraries—including dataclasses, enum, and Pydantic—use __init_subclass__ to provide elegant, low-boilerplate APIs.
How __init_subclass__ Works
When a class is defined with a parent that implements __init_subclass__, Python automatically calls Parent.__init_subclass__(**kwargs) passing any keyword arguments from the class definition. This happens at class definition time, before any instances are created:
class Plugin:
"""Base class for all plugins."""
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
print(f'Registering plugin: {cls.__name__}')
class FileExporter(Plugin):
pass
# Output (at definition time): Registering plugin: FileExporter
class DatabaseExporter(Plugin):
pass
# Output: Registering plugin: DatabaseExporter
The cls parameter is the subclass being defined. The **kwargs allows subclasses to pass configuration. This is simpler than decorators because you don't need to explicitly apply it to each subclass—it's automatic.
Building a Plugin Registry with __init_subclass__
Here's a practical plugin system that uses __init_subclass__ for auto-registration:
class PluginBase:
"""Base class for all plugins. Subclasses auto-register."""
_registry = {}
def __init_subclass__(cls, plugin_name=None, **kwargs):
super().__init_subclass__(**kwargs)
# Use explicit plugin_name or derive from class name
name = plugin_name or cls.__name__
if name in PluginBase._registry:
raise ValueError(f'Plugin {name} already registered')
PluginBase._registry[name] = cls
cls._plugin_name = name
@classmethod
def get_plugin(cls, name):
"""Retrieve a plugin by name."""
return cls._registry.get(name)
@classmethod
def list_plugins(cls):
"""List all registered plugins."""
return list(cls._registry.keys())
class JSONExporter(PluginBase, plugin_name='json'):
def export(self, data):
import json
return json.dumps(data, indent=2)
class XMLExporter(PluginBase, plugin_name='xml'):
def export(self, data):
# Simplified XML export
return f'<data>{data}</data>'
# Auto-derived name from class name
class CSVExporter(PluginBase):
def export(self, data):
return 'col1,col2\n' + str(data)
# Usage
print(PluginBase.list_plugins()) # Output: ['json', 'xml', 'CSVExporter']
exporter = PluginBase.get_plugin('json')()
print(exporter.export({'name': 'Alice', 'age': 30}))
Every subclass of PluginBase is instantly registered without decorator syntax. This is how frameworks like Celery and pytest discover plugins.
Enforcing Naming Conventions with __init_subclass__
Use __init_subclass__ to validate or enforce class design rules:
class APIEndpoint:
"""Base class for REST endpoints. Enforces naming convention."""
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
# Enforce that subclass names end with 'Endpoint'
if not cls.__name__.endswith('Endpoint'):
raise TypeError(
f'{cls.__name__} must end with "Endpoint" '
f'(e.g., UsersEndpoint)'
)
# Enforce that subclasses implement required methods
required_methods = ['get', 'post']
for method in required_methods:
if not hasattr(cls, method) or not callable(getattr(cls, method)):
raise TypeError(
f'{cls.__name__} must implement {method}() method'
)
class UsersEndpoint(APIEndpoint):
def get(self):
return {'method': 'GET', 'path': '/users'}
def post(self):
return {'method': 'POST', 'path': '/users'}
# This raises TypeError: name doesn't end with 'Endpoint'
try:
class UserService(APIEndpoint):
def get(self):
pass
def post(self):
pass
except TypeError as e:
print(f'Error: {e}')
By enforcing constraints in __init_subclass__, you catch configuration errors early and provide clear error messages to framework users.
Adding Metadata to Subclasses
__init_subclass__ lets you augment subclasses with derived metadata:
class Model:
"""Base model class. Subclasses get automatic __repr__ and field tracking."""
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
# Track all type-annotated fields
cls._fields = {}
for attr_name, attr_type in getattr(cls, '__annotations__', {}).items():
cls._fields[attr_name] = attr_type
# Auto-generate __repr__
def __repr__(self):
fields = ', '.join(
f'{k}={getattr(self, k, None)!r}'
for k in cls._fields
)
return f'{cls.__name__}({fields})'
cls.__repr__ = __repr__
# Auto-generate __eq__
def __eq__(self, other):
if not isinstance(other, cls):
return False
return all(
getattr(self, k) == getattr(other, k)
for k in cls._fields
)
cls.__eq__ = __eq__
class Person(Model):
name: str
age: int
email: str
person1 = Person()
person1.name = "Alice"
person1.age = 30
person1.email = "[email protected]"
person2 = Person()
person2.name = "Alice"
person2.age = 30
person2.email = "[email protected]"
print(person1) # Output: Person(name='Alice', age=30, email='[email protected]')
print(person1 == person2) # Output: True
This pattern is similar to @dataclass: __init_subclass__ derives methods and metadata from type annotations, creating a lightweight data model without explicit method definitions.
Comparison: __init_subclass__ vs. Metaclasses vs. Decorators
| Mechanism | When Called | Use Case | Complexity |
|---|---|---|---|
__init_subclass__ | At subclass definition time | Validation, registration, metadata | Low |
| Class decorator | At class definition time | Similar to __init_subclass__ but explicit | Low |
| Metaclass | During class creation (before definition completes) | Control class creation, enforce hierarchy-wide rules | High |
Key Takeaways
__init_subclass__is called automatically when a subclass is defined, without requiring decorators or metaclasses.- Use
__init_subclass__for plugin registries, automatic registration, and enforcing design rules across a hierarchy. - Pass keyword arguments in the class definition:
class MyClass(Base, option=value):and receive them in__init_subclass__(**kwargs). - Always call
super().__init_subclass__(**kwargs)to support multiple inheritance and parent class hooks.
Frequently Asked Questions
How do I pass arguments to __init_subclass__?
Add them as keyword arguments in the class definition: class MyClass(Base, name='custom', enabled=True): and receive them in __init_subclass__(**kwargs).
Does __init_subclass__ run every time an instance is created?
No. It runs once at class definition time (when the subclass is first defined), not when instances are created. This makes it efficient for registration and metadata setup.
Can __init_subclass__ prevent a subclass from being created?
Yes. Raise an exception inside __init_subclass__ (like TypeError or ValueError) to prevent the subclass definition from completing.
How does __init_subclass__ interact with multiple inheritance?
If multiple parent classes define __init_subclass__, all of them are called in MRO order. Always call super().__init_subclass__(**kwargs) to ensure all parent hooks run.
What's the difference between __init_subclass__ and __init__?
__init_subclass__ is called once at subclass definition time and receives the subclass as cls. __init__ is called every time an instance is created and receives the instance as self.
Further Reading
- PEP 487: Simpler customization of class creation — official specification and motivation for
__init_subclass__. - Python Data Model: init_subclass — official documentation.
- Dataclasses Implementation — the stdlib dataclasses module uses
__init_subclass__. - Building Frameworks with init_subclass — Real Python guide to advanced class customization.