In software architecture, specifically when building frameworks or plugin systems, you often need a way to keep track of every class that inherits from a specific base class. Manually adding every new class to a list is error-prone and tedious.
Python provides powerful metaprogramming tools to automate this. This guide explores three methods to automatically register classes upon definition: the modern __init_subclass__ hook, class decorators, and the lower-level metaclasses.
Why Track Classes?
Class tracking is useful when you need to:
- Discover Plugins: Automatically find all available plugins without importing them manually in a config file.
- Dispatch Events: Route data to specific handlers based on class attributes.
- Validate Subclasses: Ensure subclasses implement specific methods or attributes at definition time.
Method 1: Using __init_subclass__ (Recommended)
Introduced in Python 3.6, __init_subclass__ is the cleanest way to track classes. It is a special class method defined in a base class that gets called whenever a subclass is defined.
The Implementation:
class PluginBase:
registry = {}
def __init_subclass__(cls, **kwargs):
"""
Hook that fires when a class inherits from PluginBase.
"""
super().__init_subclass__(**kwargs)
PluginBase.registry[cls.__name__] = cls
print(f"Registered plugin: {cls.__name__}")
class AudioPlugin(PluginBase):
pass
class VideoPlugin(PluginBase):
pass
print("\nRegistry Content:")
print(PluginBase.registry)Output:
Registered plugin: AudioPlugin
Registered plugin: VideoPlugin
Registry Content:
{'AudioPlugin': <class '__main__.AudioPlugin'>, 'VideoPlugin': <class '__main__.VideoPlugin'>}Method 2: Using Class Decorators
If you do not want to force inheritance or want to be selective about which classes are tracked, decorators are a great alternative. This method is explicit: a developer must intentionally tag a class to register it.
The Implementation:
plugin_registry = {}
def register_plugin(cls):
"""Class decorator to register a class."""
plugin_registry[cls.__name__] = cls
return cls
@register_plugin
class DatabaseConnector:
pass
@register_plugin
class NetworkConnector:
pass
class LocalFileConnector:
pass
print(f"Tracked Classes: {list(plugin_registry.keys())}")Output:
Tracked Classes: ['DatabaseConnector', 'NetworkConnector']Method 3: Using Metaclasses
Metaclasses are the "classes of classes." They control the creation of class objects. While powerful, they are often overkill for simple tracking. However, they allow you to intercept the class definition before it is fully created.
The Implementation:
class TrackingMeta(type):
"""Metaclass that maintains a registry of all classes it creates."""
_registry = {}
def __new__(cls, name, bases, attrs):
new_class = super().__new__(cls, name, bases, attrs)
if name != 'BaseMetaPlugin':
cls._registry[name] = new_class
return new_class
class BaseMetaPlugin(metaclass=TrackingMeta):
pass
class ImageResizePlugin(BaseMetaPlugin):
pass
print(f"Metaclass Registry: {TrackingMeta._registry}")Output:
Metaclass Registry: {'ImageResizePlugin': <class '__main__.ImageResizePlugin'>}Real-World Example: A Simple Plugin System
Let's build a file processor system where we can get the correct processor class based on a file extension string.
class FileProcessor:
_processors = {}
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
if hasattr(cls, 'ext'):
cls._processors[cls.ext] = cls
@classmethod
def get_processor(cls, filename):
"""Factory method to get the right class instance."""
extension = filename.split('.')[-1]
processor_class = cls._processors.get(extension)
if processor_class:
return processor_class()
else:
raise ValueError(f"No processor found for .{extension}")
def process(self, filename):
raise NotImplementedError
class TextProcessor(FileProcessor):
ext = 'txt'
def process(self, filename):
return f"Processing text file: {filename}"
class CSVProcessor(FileProcessor):
ext = 'csv'
def process(self, filename):
return f"Parsing CSV data: {filename}"
try:
processor = FileProcessor.get_processor("data.csv")
print(processor.process("data.csv"))
processor = FileProcessor.get_processor("readme.txt")
print(processor.process("readme.txt"))
except ValueError as e:
print(e)Output:
Parsing CSV data: data.csv
Processing text file: readme.txtConclusion
Automating class tracking makes your code extensible and reduces boilerplate configuration.
- Use
__init_subclass__for most inheritance-based hierarchies (Python 3.6+). It is simple and robust. - Use Decorators if you want an opt-in system where tracking is explicit.
- Use Metaclasses only if you need deep control over class creation attributes before the class exists.