Introduction to Metaclasses
Metaclasses are "classes of classes." Just as ordinary classes create instances, metaclasses create and control classes. Every class in Python is fundamentally an instance of type.
type — Python's Default Metaclass
# Check the type of a class with type()
print(type(42)) # <class 'int'>
print(type("hello")) # <class 'str'>
print(type(int)) # <class 'type'>
print(type(type)) # <class 'type'> (type is also an instance of itself)
# Dynamically create a class with type()
# type(name, bases, attribute_dict)
MyClass = type(
"MyClass", # Class name
(object,), # Tuple of base classes
{
"x": 10, # Class attribute
"greet": lambda self: f"Hello! x={self.x}", # Method
}
)
obj = MyClass()
print(obj.greet()) # Hello! x=10
print(type(obj)) # <class '__main__.MyClass'>
print(type(MyClass)) # <class 'type'>
# Equivalent to a class defined with the class statement
class EquivalentClass:
x = 10
def greet(self):
return f"Hello! x={self.x}"
print(MyClass.__bases__) # (<class 'object'>,)
print(EquivalentClass.__bases__) # (<class 'object'>,)
Defining a Metaclass
class MyMeta(type):
"""Custom metaclass — inherits from type"""
def __new__(mcs, name, bases, namespace):
"""Called before the class object is created"""
print(f"[MetaNew] Creating class: {name}")
# namespace: dict of attributes defined in the class body
cls = super().__new__(mcs, name, bases, namespace)
return cls
def __init__(cls, name, bases, namespace):
"""Called after the class object is created"""
print(f"[MetaInit] Initializing class: {name}")
super().__init__(name, bases, namespace)
def __call__(cls, *args, **kwargs):
"""Called when cls() is invoked (instance creation)"""
print(f"[MetaCall] Creating instance of {cls.__name__}")
instance = super().__call__(*args, **kwargs)
return instance
class MyClass(metaclass=MyMeta):
def __init__(self, value: int):
self.value = value
print("--- Creating instance ---")
obj = MyClass(42)
print(f"value: {obj.value}")
Real-World Metaclass Patterns
1. Singleton Metaclass
class SingletonMeta(type):
_instances: dict = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super().__call__(*args, **kwargs)
return cls._instances[cls]
class AppConfig(metaclass=SingletonMeta):
def __init__(self):
self.debug = False
self.db_url = "sqlite:///app.db"
print("AppConfig initialized")
class Logger(metaclass=SingletonMeta):
def __init__(self):
self._logs: list[str] = []
print("Logger initialized")
def log(self, msg: str) -> None:
self._logs.append(msg)
config1 = AppConfig() # Initialized
config2 = AppConfig() # Returns existing instance
print(config1 is config2) # True
logger1 = Logger()
logger2 = Logger()
logger1.log("First log")
print(f"Log count: {len(logger2._logs)}") # 1 (same instance)
2. Abstract Method Enforcement Metaclass
class InterfaceMeta(type):
"""Metaclass that enforces implementation of specified methods"""
def __new__(mcs, name, bases, namespace):
cls = super().__new__(mcs, name, bases, namespace)
# Only check subclasses (skip the interface class itself)
if bases:
required = set()
for base in bases:
required.update(getattr(base, "_required_methods", set()))
missing = required - set(namespace)
if missing:
raise TypeError(
f"{name} must implement the following methods: {missing}"
)
return cls
class Animal(metaclass=InterfaceMeta):
_required_methods = {"speak", "move"}
def speak(self) -> str:
raise NotImplementedError
def move(self) -> str:
raise NotImplementedError
class Dog(Animal):
def speak(self) -> str:
return "Woof!"
def move(self) -> str:
return "Running!"
try:
class Fish(Animal):
def speak(self) -> str:
return "..."
# move not implemented → TypeError
except TypeError as e:
print(f"Error: {e}")
dog = Dog()
print(dog.speak())
3. Auto Attribute Conversion Metaclass
class SnakeCaseMeta(type):
"""Metaclass that automatically converts camelCase methods to snake_case"""
@staticmethod
def _to_snake(name: str) -> str:
import re
s1 = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", name)
return re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1).lower()
def __new__(mcs, name, bases, namespace):
new_namespace = {}
for key, value in namespace.items():
if not key.startswith("_") and callable(value):
snake_key = mcs._to_snake(key)
if snake_key != key:
new_namespace[snake_key] = value # Add snake_case version
new_namespace[key] = value # Keep original for compatibility
else:
new_namespace[key] = value
else:
new_namespace[key] = value
return super().__new__(mcs, name, bases, new_namespace)
class APIClient(metaclass=SnakeCaseMeta):
def getUserById(self, user_id: int) -> dict:
return {"id": user_id, "name": "Alice"}
def createNewOrder(self, items: list) -> dict:
return {"order_id": 1, "items": items}
def deleteExpiredSessions(self) -> int:
return 5
client = APIClient()
# Both camelCase and snake_case work
print(client.getUserById(1))
print(client.get_user_by_id(1)) # snake_case version
print(client.createNewOrder(["book"]))
print(client.create_new_order(["book"])) # snake_case version
__new__ vs __init__ Difference
class TrackedType(type):
"""Metaclass that tracks all created classes"""
_all_classes: list = []
def __new__(mcs, name, bases, namespace):
cls = super().__new__(mcs, name, bases, namespace)
TrackedType._all_classes.append(cls)
return cls
def __init__(cls, name, bases, namespace):
super().__init__(name, bases, namespace)
# Class already created in __new__ — additional setup in __init__
cls._created_at = __import__("time").time()
@classmethod
def get_all_classes(mcs) -> list:
return list(mcs._all_classes)
class Base(metaclass=TrackedType):
pass
class Child(Base):
pass
class AnotherClass(metaclass=TrackedType):
pass
print("Tracked classes:")
for cls in TrackedType.get_all_classes():
print(f" {cls.__name__}")
__init_subclass__ — Alternative to Metaclasses
Introduced in Python 3.6+, __init_subclass__ is far simpler than metaclasses for straightforward use cases like subclass registration.
class Plugin:
"""Auto-registers subclasses — no metaclass needed"""
_registry: dict[str, type] = {}
def __init_subclass__(cls, plugin_type: str = "", **kwargs):
super().__init_subclass__(**kwargs)
if plugin_type:
Plugin._registry[plugin_type] = cls
print(f"Plugin registered: {plugin_type} → {cls.__name__}")
@classmethod
def create(cls, plugin_type: str) -> "Plugin":
klass = cls._registry.get(plugin_type)
if klass is None:
available = list(cls._registry)
raise KeyError(f"Unknown plugin: {plugin_type}. Available: {available}")
return klass()
def execute(self) -> str:
raise NotImplementedError
class CSVPlugin(Plugin, plugin_type="csv"):
def execute(self) -> str:
return "CSV processing complete"
class JSONPlugin(Plugin, plugin_type="json"):
def execute(self) -> str:
return "JSON processing complete"
class XMLPlugin(Plugin, plugin_type="xml"):
def execute(self) -> str:
return "XML processing complete"
for pt in ["csv", "json", "xml"]:
p = Plugin.create(pt)
print(p.execute())
Metaclass in Practice: ORM Style
class Field:
"""ORM field base class"""
def __init__(self, field_type: type, required: bool = True, default=None):
self.field_type = field_type
self.required = required
self.default = default
self.name = None # Set by __set_name__
def __set_name__(self, owner, name: str):
self.name = name
def validate(self, value):
if value is None:
if self.required and self.default is None:
raise ValueError(f"{self.name}: this field is required.")
return self.default
if not isinstance(value, self.field_type):
raise TypeError(
f"{self.name}: expected {self.field_type.__name__}, "
f"got {type(value).__name__}"
)
return value
class IntField(Field):
def __init__(self, required: bool = True, min_val=None, max_val=None, **kwargs):
super().__init__(int, required, **kwargs)
self.min_val = min_val
self.max_val = max_val
def validate(self, value):
value = super().validate(value)
if value is not None:
if self.min_val is not None and value < self.min_val:
raise ValueError(f"{self.name}: {value} < min {self.min_val}")
if self.max_val is not None and value > self.max_val:
raise ValueError(f"{self.name}: {value} > max {self.max_val}")
return value
class StrField(Field):
def __init__(self, required: bool = True, max_length: int = None, **kwargs):
super().__init__(str, required, **kwargs)
self.max_length = max_length
def validate(self, value):
value = super().validate(value)
if value is not None and self.max_length and len(value) > self.max_length:
raise ValueError(f"{self.name}: exceeds max length of {self.max_length}")
return value
class ModelMeta(type):
"""Metaclass for ORM models"""
def __new__(mcs, name, bases, namespace):
fields = {}
for key, value in namespace.items():
if isinstance(value, Field):
fields[key] = value
namespace["_fields"] = fields
return super().__new__(mcs, name, bases, namespace)
class Model(metaclass=ModelMeta):
"""ORM base model class"""
def __init__(self, **kwargs):
for field_name, field in self._fields.items():
raw_value = kwargs.get(field_name)
validated = field.validate(raw_value)
object.__setattr__(self, field_name, validated)
def __repr__(self) -> str:
attrs = ", ".join(
f"{k}={getattr(self, k)!r}"
for k in self._fields
)
return f"{self.__class__.__name__}({attrs})"
def to_dict(self) -> dict:
return {k: getattr(self, k) for k in self._fields}
@classmethod
def fields(cls) -> list[str]:
return list(cls._fields)
class User(Model):
name = StrField(max_length=50)
email = StrField(max_length=100)
age = IntField(min_val=0, max_val=150, required=False, default=0)
score = IntField(min_val=0, max_val=100, required=False, default=50)
# Valid creation
u = User(name="Alice", email="alice@example.com", age=30)
print(u)
print(u.to_dict())
# Using defaults
u2 = User(name="Bob", email="bob@example.com")
print(u2) # age=0, score=50
# Validation error
try:
User(name="Charlie", email="charlie@example.com", age=200)
except ValueError as e:
print(f"Error: {e}")
try:
User(name="X" * 100, email="x@example.com")
except ValueError as e:
print(f"Error: {e}")
Pro Tips
1. When to Use Metaclasses
# Metaclass is needed:
# - When you need to control class creation itself
# - When applying common logic transparently to all subclasses
# - When analyzing class definitions, like ORM or serialization frameworks
# Metaclass NOT needed (use alternatives):
# - Simply adding attributes → class decorator
# - Registering subclasses → __init_subclass__
# - Validating attributes → descriptors
# - Sharing instances → module-level variables
# "Metaclasses are deeper magic than 99% of users should ever worry about." — Tim Peters
2. Resolving Metaclass Conflicts
# When you need to use two metaclasses together, combine them into a new metaclass
class MetaA(type):
def __new__(mcs, name, bases, ns):
print(f"MetaA: {name}")
return super().__new__(mcs, name, bases, ns)
class MetaB(type):
def __new__(mcs, name, bases, ns):
print(f"MetaB: {name}")
return super().__new__(mcs, name, bases, ns)
# New metaclass that inherits from both
class CombinedMeta(MetaA, MetaB):
pass
class MyClass(metaclass=CombinedMeta):
pass
Summary
| Feature | Metaclass | Alternative |
|---|---|---|
| Control class creation | type.__new__ | — |
| Register subclasses | __new__ | __init_subclass__ (recommended) |
| Validate attributes | __new__ | Descriptors (recommended) |
| Decorate classes | __init__ | Class decorators (recommended) |
| Singleton | __call__ | Class decorator or module variable |
Metaclasses are powerful but complex. Where possible, first consider simpler alternatives like __init_subclass__, descriptors, and class decorators.