Metaclasses

Metaclasses are classes whose instances are classes. They control class creation, allowing you to modify class behavior at definition time.

What is a Metaclass?

Everything in Python is an object, including classes. A metaclass defines how classes are created:

# type is the default metaclass
print(type("MyClass", (), {}))  # Creates a class
print(type(type))  # type is its own metaclass

class MyMeta(type):
    pass

class MyClass(metaclass=MyMeta):
    pass

print(type(MyClass))  # MyMeta
print(MyClass.__class__)  # MyMeta

Class Creation Hooks

Metaclasses intercept class definition with __new__ and __init__:

class EntityMeta(type):
    def __new__(mcs, name, bases, namespace):
        # mcs - the metaclass (self for the metaclass)
        # name - class name
        # bases - tuple of base classes
        # namespace - class attributes/methods dict
        
        # Can modify namespace before class creation
        if 'validate' not in namespace:
            namespace['_is_valid'] = True
        
        cls = super().__new__(mcs, name, bases, namespace)
        return cls

class Entity(metaclass=EntityMeta):
    def __init__(self, data):
        self.data = data

e = Entity("test")
print(e._is_valid)  # True - added by metaclass

__init_subclass__

Simpler than full metaclasses - called when a class is subclassed:

class Registry:
    _registry = {}
    
    def __init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)
        if cls.__name__ not in cls._registry:
            cls._registry[cls.__name__] = cls

class PluginA(Registry):
    pass

class PluginB(Registry):
    pass

print(Registry._registry)  # {'PluginA': ..., 'PluginB': ...}

Tracking Class Attributes

Automatically track attributes across all instances:

class AutoAttrMeta(type):
    def __new__(mcs, name, bases, namespace):
        # Track all attribute names
        attrs = []
        for key, value in namespace.items():
            if not key.startswith('_') and callable(value):
                attrs.append(key)
        
        # Add _attributes to class
        namespace['_attributes'] = attrs
        
        return super().__new__(mcs, name, bases, namespace)

class Model(metaclass=AutoAttrMeta):
    def save(self): pass
    def delete(self): pass
    def update(self): pass

print(Model._attributes)  # ['save', 'delete', 'update']

Singleton Pattern

Use metaclass for singletons:

class SingletonMeta(type):
    _instances = {}
    
    def __call__(cls, **kwargs):
        if cls not in cls._instances:
            cls._instances[cls] = super().__call__(**kwargs)
        return cls._instances[cls]

class Config(metaclass=SingletonMeta):
    def __init__(self, **kwargs):
        for k, v in kwargs.items():
            setattr(self, k, v)

config1 = Config(host="localhost", port=8080)
config2 = Config(host="other")

print(config1.host)  # localhost
print(config2.host)  # localhost - same instance!
print(config1 is config2)  # True

Abstract Base Classes with Metaclass

Create custom abstract class systems:

class ABCMeta(type):
    _abstract_methods = {}
    
    def __new__(mcs, name, bases, namespace):
        cls = super().__new__(mcs, name, bases, namespace)
        
        # Collect abstract methods from bases
        abstract_methods = set()
        for base in bases:
            if hasattr(base, '_abstract_methods'):
                abstract_methods.update(base._abstract_methods)
        
        # Check this class for abstract methods
        for attr in namespace.get('_abstract_', []):
            abstract_methods.add(attr)
        
        cls._abstract_methods = abstract_methods
        return cls

class Base(metaclass=ABCMeta):
    _abstract_ = ['process']
    
    def run(self):
        return self.process()

class Concrete(Base):
    def process(self):
        return "done"

c = Concrete()
print(c.run())  # done

When to Use Metaclasses

Use Case Recommended Approach
Register classes __init_subclass__
Modify class at creation Metaclass
Singleton Metaclass or closure
ORM models Metaclass
Plugin system __init_subclass__
Tip: Prefer __init_subclass__ when possible. It's simpler and often sufficient.