Metaclasses

What are Metaclasses?

Metaclasses are "classes that create classes." They define how classes are constructed and behave.

# Everything in Python is an object, including classes
class MyClass:
    pass

print(type(MyClass))        # <class 'type'>
print(type(type))           # <class 'type'>

# Classes are instances of 'type' (the default metaclass)
# type is its own metaclass

# Creating a class dynamically with type()
def init_method(self, name):
    self.name = name

def say_hello(self):
    return f"Hello, I'm {self.name}"

# type(name, bases, dict) creates a class
DynamicClass = type('DynamicClass', (), {
    '__init__': init_method,
    'say_hello': say_hello
})

obj = DynamicClass("Alice")
print(obj.say_hello())  # Hello, I'm Alice

Custom Metaclasses

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

class Database(metaclass=SingletonMeta):
    def __init__(self):
        self.connection = "Connected to DB"

# Singleton behavior
db1 = Database()
db2 = Database()
print(db1 is db2)  # True - same instance

# Metaclass that modifies class creation
class AutoPropertyMeta(type):
    def __new__(mcs, name, bases, namespace):
        # Convert attributes starting with '_' to properties
        for key, value in list(namespace.items()):
            if key.startswith('_') and not key.startswith('__'):
                prop_name = key[1:]  # Remove leading underscore
                
                def make_property(attr_name):
                    def getter(self):
                        return getattr(self, attr_name)
                    def setter(self, value):
                        setattr(self, attr_name, value)
                    return property(getter, setter)
                
                namespace[prop_name] = make_property(key)
        
        return super().__new__(mcs, name, bases, namespace)

class Person(metaclass=AutoPropertyMeta):
    def __init__(self, name, age):
        self._name = name
        self._age = age

person = Person("Alice", 25)
print(person.name)  # Alice (automatically created property)
person.age = 26     # Uses auto-generated setter

Metaclass Methods

class ValidatedMeta(type):
    def __new__(mcs, name, bases, namespace):
        # Called when class is created
        print(f"Creating class {name}")
        
        # Validate that all methods have docstrings
        for key, value in namespace.items():
            if callable(value) and not key.startswith('_'):
                if not getattr(value, '__doc__', None):
                    raise ValueError(f"Method {key} must have a docstring")
        
        return super().__new__(mcs, name, bases, namespace)
    
    def __init__(cls, name, bases, namespace):
        # Called after class is created
        print(f"Initializing class {name}")
        super().__init__(name, bases, namespace)
    
    def __call__(cls, *args, **kwargs):
        # Called when creating instances
        print(f"Creating instance of {cls.__name__}")
        return super().__call__(*args, **kwargs)

class ValidatedClass(metaclass=ValidatedMeta):
    def greet(self):
        """Greet the user"""
        return "Hello!"
    
    def farewell(self):
        """Say goodbye"""
        return "Goodbye!"

# This would raise an error:
# class InvalidClass(metaclass=ValidatedMeta):
#     def no_docstring(self):  # Missing docstring!
#         pass

Practical Metaclass Examples

# ORM-like metaclass
class ModelMeta(type):
    def __new__(mcs, name, bases, namespace):
        # Collect field definitions
        fields = {}
        for key, value in list(namespace.items()):
            if isinstance(value, Field):
                fields[key] = value
                namespace.pop(key)  # Remove from class namespace
        
        namespace['_fields'] = fields
        return super().__new__(mcs, name, bases, namespace)

class Field:
    def __init__(self, field_type, required=True):
        self.field_type = field_type
        self.required = required

class Model(metaclass=ModelMeta):
    def __init__(self, **kwargs):
        for name, field in self._fields.items():
            if name in kwargs:
                value = kwargs[name]
                if not isinstance(value, field.field_type):
                    raise TypeError(f"{name} must be {field.field_type.__name__}")
                setattr(self, name, value)
            elif field.required:
                raise ValueError(f"{name} is required")

class User(Model):
    name = Field(str)
    age = Field(int)
    email = Field(str, required=False)

user = User(name="Alice", age=25)
print(user.name)  # Alice

# Registry metaclass
class RegistryMeta(type):
    registry = {}
    
    def __new__(mcs, name, bases, namespace):
        cls = super().__new__(mcs, name, bases, namespace)
        if name != 'RegisteredClass':  # Don't register base class
            mcs.registry[name] = cls
        return cls
    
    @classmethod
    def get_class(mcs, name):
        return mcs.registry.get(name)

class RegisteredClass(metaclass=RegistryMeta):
    pass

class PluginA(RegisteredClass):
    def process(self):
        return "Processing with Plugin A"

class PluginB(RegisteredClass):
    def process(self):
        return "Processing with Plugin B"

# Access classes by name
plugin_class = RegistryMeta.get_class('PluginA')
plugin = plugin_class()
print(plugin.process())  # Processing with Plugin A

Metaclass vs Class Decorators

# Often, class decorators are simpler than metaclasses

# Using metaclass
class AddMethodMeta(type):
    def __new__(mcs, name, bases, namespace):
        def added_method(self):
            return f"Added method in {self.__class__.__name__}"
        namespace['added_method'] = added_method
        return super().__new__(mcs, name, bases, namespace)

class WithMeta(metaclass=AddMethodMeta):
    pass

# Using class decorator (simpler)
def add_method(cls):
    def added_method(self):
        return f"Added method in {self.__class__.__name__}"
    cls.added_method = added_method
    return cls

@add_method
class WithDecorator:
    pass

# Both achieve the same result
obj1 = WithMeta()
obj2 = WithDecorator()
print(obj1.added_method())  # Added method in WithMeta
print(obj2.added_method())  # Added method in WithDecorator

# When to use metaclasses vs decorators:
# - Use decorators for simple modifications
# - Use metaclasses when you need to control class creation process
# - Use metaclasses for complex inheritance scenarios

Abstract Base Classes with Metaclasses

from abc import ABCMeta, abstractmethod

# Using ABCMeta (built-in metaclass)
class Shape(metaclass=ABCMeta):
    @abstractmethod
    def area(self):
        pass
    
    @abstractmethod
    def perimeter(self):
        pass

# This would raise TypeError:
# shape = Shape()  # Can't instantiate abstract class

class Rectangle(Shape):
    def __init__(self, width, height):
        self.width = width
        self.height = height
    
    def area(self):
        return self.width * self.height
    
    def perimeter(self):
        return 2 * (self.width + self.height)

# Custom abstract metaclass
class AbstractMeta(type):
    def __call__(cls, *args, **kwargs):
        # Check if all abstract methods are implemented
        abstract_methods = getattr(cls, '_abstract_methods', set())
        for method in abstract_methods:
            if not hasattr(cls, method) or getattr(cls, method) is NotImplemented:
                raise TypeError(f"Can't instantiate {cls.__name__} with abstract method {method}")
        return super().__call__(*args, **kwargs)

class AbstractBase(metaclass=AbstractMeta):
    _abstract_methods = {'process'}
    
    def process(self):
        return NotImplemented

class ConcreteClass(AbstractBase):
    def process(self):
        return "Processing..."

# concrete = ConcreteClass()  # Works
# abstract = AbstractBase()   # Would raise TypeError
⚠️ Metaclass Complexity:

Metaclasses are powerful but complex. As Tim Peters said: "Metaclasses are deeper magic than 99% of users should ever worry about."

💡 When to Use Metaclasses:
← Async Programming Back to Home