Skip to main content
Advertisement

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

FeatureMetaclassAlternative
Control class creationtype.__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.

Advertisement