Skip to content

How to Automate Class Tracking in Python

Learn how to automatically register and track subclasses in Python using __init_subclass__, decorators, or metaclasses for plugin systems, event dispatch, and validation.

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.

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.txt

Conclusion

Automating class tracking makes your code extensible and reduces boilerplate configuration.

  1. Use __init_subclass__ for most inheritance-based hierarchies (Python 3.6+). It is simple and robust.
  2. Use Decorators if you want an opt-in system where tracking is explicit.
  3. Use Metaclasses only if you need deep control over class creation attributes before the class exists.
Navigation