Skip to main content

How to Automate Class Tracking in Python

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:
# This dictionary will hold references to all subclasses
registry = {}

def __init_subclass__(cls, **kwargs):
"""
Hook that fires when a class inherits from PluginBase.
"""
super().__init_subclass__(**kwargs)

# ✅ Correct: Register the class using its name as the key
PluginBase.registry[cls.__name__] = cls
print(f"Registered plugin: {cls.__name__}")

# Defining subclasses automatically triggers the hook
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'>}
note

Always call super().__init_subclass__(**kwargs) to ensure that if your class is part of a complex multiple-inheritance hierarchy, other classes in the chain also get initialized correctly.

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:

# The Registry
plugin_registry = {}

def register_plugin(cls):
"""Class decorator to register a class."""
plugin_registry[cls.__name__] = cls
# Must return the class to ensure it remains valid
return cls

# ✅ Correct: Explicitly decorating the class
@register_plugin
class DatabaseConnector:
pass

# ✅ Correct: Another tracked class
@register_plugin
class NetworkConnector:
pass

# ⛔️ Incorrect: This class is NOT tracked because it lacks the decorator
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):
# Create the new class object
new_class = super().__new__(cls, name, bases, attrs)

# Avoid registering the Base class itself
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'>}
warning

Metaclasses can cause conflicts if you try to combine two different libraries that both use custom metaclasses. Use __init_subclass__ whenever possible to avoid "metaclass conflict" errors.

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:
# Registry mapping extension -> class
_processors = {}

def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)

# Check if the subclass has a 'ext' attribute
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

# Define Plugins
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}"

# Usage
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.