Python Metaclasses
Understand Python's type system, metaclasses, __new__ and __init__, ABCMeta, and practical metaclass use cases.
Everything Is an Object
In Python, classes are objects too. The type of a class is its metaclass — by default, type.
class Dog:
def bark(self):
return "Woof"
type(Dog) # <class 'type'>
type(Dog()) # <class '__main__.Dog'>
isinstance(Dog, type) # True — Dog is an instance of type
type is both the metaclass of all classes and a built-in function. It’s the default metaclass.
type() as a Class Factory
You can create classes dynamically with type(name, bases, namespace):
# Dynamically create a class
Dog = type("Dog", (object,), {
"sound": "Woof",
"bark": lambda self: self.sound,
})
d = Dog()
d.bark() # "Woof"
# Equivalent to:
class Dog:
sound = "Woof"
def bark(self):
return self.sound
This is exactly what Python does internally when it processes a class statement.
new vs init
class Celsius:
"""An immutable temperature that refuses values below absolute zero."""
def __new__(cls, value):
if value < -273.15:
raise ValueError(f"{value}°C is below absolute zero")
instance = super().__new__(cls)
return instance
def __init__(self, value):
self._value = value
@property
def value(self):
return self._value
def to_fahrenheit(self):
return self._value * 9/5 + 32
c = Celsius(100) # fine
c = Celsius(-300) # raises ValueError
__new__ is also used for immutable types (subclassing int, str, tuple) where __init__ is too late:
class PositiveInt(int):
def __new__(cls, value):
if value <= 0:
raise ValueError("Must be positive")
return super().__new__(cls, value)
x = PositiveInt(5) # fine
y = PositiveInt(-1) # raises ValueError
Writing a Metaclass
A metaclass is a class whose instances are classes. Create one by inheriting from type:
class SingletonMeta(type):
"""Metaclass that enforces the Singleton pattern."""
_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, url):
self.url = url
self.connected = False
db1 = Database("postgresql://localhost/app")
db2 = Database("different url")
db1 is db2 # True — same instance
Metaclass new and init
class ValidatedMeta(type):
"""Enforce that all public methods have docstrings."""
def __new__(mcs, name, bases, namespace):
for attr_name, attr_value in namespace.items():
if callable(attr_value) and not attr_name.startswith("_"):
if not attr_value.__doc__:
raise TypeError(
f"{name}.{attr_name} is missing a docstring"
)
return super().__new__(mcs, name, bases, namespace)
class MyAPI(metaclass=ValidatedMeta):
def get_user(self, user_id):
"""Fetch a user by ID."""
...
def delete_user(self, user_id):
# Missing docstring — raises TypeError at class definition time
...
ABCMeta and Abstract Base Classes
ABCMeta is a metaclass that adds the @abstractmethod mechanism:
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self) -> float:
"""Return the area of the shape."""
...
@abstractmethod
def perimeter(self) -> float:
"""Return the perimeter of the shape."""
...
def describe(self) -> str:
return f"Area: {self.area():.2f}, Perimeter: {self.perimeter():.2f}"
class Circle(Shape):
def __init__(self, radius: float):
self.radius = radius
def area(self) -> float:
import math
return math.pi * self.radius ** 2
def perimeter(self) -> float:
import math
return 2 * math.pi * self.radius
# Shape() → TypeError: Can't instantiate abstract class
# Circle(5) → fine
c = Circle(5)
c.describe() # "Area: 78.54, Perimeter: 31.42"
Abstract Properties and Class Methods
from abc import ABC, abstractmethod
class DataStore(ABC):
@property
@abstractmethod
def connection_string(self) -> str:
...
@classmethod
@abstractmethod
def from_env(cls) -> "DataStore":
...
@abstractmethod
def connect(self) -> None:
...
init_subclass: A Simpler Alternative
For many metaclass use cases, __init_subclass__ is cleaner:
class Plugin:
_registry: dict[str, type] = {}
def __init_subclass__(cls, plugin_name: str = "", **kwargs):
super().__init_subclass__(**kwargs)
if plugin_name:
Plugin._registry[plugin_name] = cls
class JSONPlugin(Plugin, plugin_name="json"):
def process(self, data): ...
class XMLPlugin(Plugin, plugin_name="xml"):
def process(self, data): ...
Plugin._registry
# {"json": JSONPlugin, "xml": XMLPlugin}
# Auto-registration without metaclass boilerplate
def get_plugin(name: str) -> Plugin:
cls = Plugin._registry.get(name)
if cls is None:
raise ValueError(f"Unknown plugin: {name}")
return cls()
Class Decorators: Often Better Than Metaclasses
def enforce_types(cls):
"""Add runtime type checking to all __init__ parameters."""
import inspect
original_init = cls.__init__
sig = inspect.signature(original_init)
def checked_init(self, *args, **kwargs):
bound = sig.bind(self, *args, **kwargs)
bound.apply_defaults()
hints = cls.__init__.__annotations__
for param, value in list(bound.arguments.items())[1:]: # skip self
if param in hints and not isinstance(value, hints[param]):
raise TypeError(
f"{param} must be {hints[param].__name__}, "
f"got {type(value).__name__}"
)
original_init(self, *args, **kwargs)
cls.__init__ = checked_init
return cls
@enforce_types
class User:
def __init__(self, name: str, age: int):
self.name = name
self.age = age
User("Alice", 30) # fine
User("Alice", "30") # TypeError: age must be int, got str
When to Use What
| Tool | Use When |
|---|---|
__init_subclass__ | Plugin registries, validating subclasses |
| Class decorator | Modifying or wrapping a class without inheritance |
ABCMeta / ABC | Defining interfaces / abstract base classes |
| Custom metaclass | Framework-level class creation (ORMs, schema validators) |