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 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
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
# 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
# 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
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
Metaclasses are powerful but complex. As Tim Peters said: "Metaclasses are deeper magic than 99% of users should ever worry about."