Skip to main content
Python advanced Lesson 26 of 28

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

ToolUse When
__init_subclass__Plugin registries, validating subclasses
Class decoratorModifying or wrapping a class without inheritance
ABCMeta / ABCDefining interfaces / abstract base classes
Custom metaclassFramework-level class creation (ORMs, schema validators)

Frequently Asked Questions

Do I need metaclasses in everyday Python?
Rarely. Most problems solved with metaclasses can be solved more simply with class decorators or __init_subclass__. Understand metaclasses to read framework code; reach for simpler tools first.
What's the difference between __new__ and __init__?
__new__ creates the object (returns the new instance). __init__ initializes it (receives the already-created instance). Override __new__ when you need to control object creation itself — like Singletons or immutable types.
What is ABCMeta used for?
ABCMeta enforces that subclasses implement abstract methods. If a class inherits from an ABC but doesn't implement all abstract methods, Python raises TypeError on instantiation.