Metaclasses
📖 Concept
In Python, everything is an object — including classes themselves. A metaclass is the "class of a class." Just as a class defines how instances behave, a metaclass defines how classes behave. By default, all classes are instances of type.
The relationship:
class Foo:
pass
type(Foo) # <class 'type'> — Foo is an instance of type
type(type) # <class 'type'> — type is its own metaclass
isinstance(Foo, type) # True
type() as a class factory:
type(name, bases, namespace) dynamically creates classes at runtime:
# These are equivalent:
class Dog:
sound = "woof"
Dog = type("Dog", (), {"sound": "woof"})
__new__ vs __init__ in metaclasses:
| Method | Called when | Receives | Purpose |
|---|---|---|---|
__new__(mcs, name, bases, namespace) |
Class is created | Class name, base classes, namespace dict | Control class creation, modify namespace |
__init__(cls, name, bases, namespace) |
Class is initialized | Same args, but class already exists | Post-creation setup |
__call__(cls, *args, **kwargs) |
Class is instantiated | Instance creation args | Control instance creation |
When to use metaclasses:
- Registering classes automatically (plugin systems, ORMs)
- Enforcing coding contracts (all subclasses must define certain methods)
- Modifying or wrapping methods at class creation time
- Implementing the descriptor protocol for frameworks
ABCMeta from the abc module is Python's built-in metaclass for abstract base classes. It tracks @abstractmethod decorators and prevents instantiation of classes that don't implement all abstract methods. You can combine custom metaclass logic with ABCMeta by inheriting from it.
The metaclass search order: Python checks for metaclass= in the class definition, then checks the first base class's metaclass, then defaults to type. If conflicting metaclasses are found, the most derived one must be a subclass of all others, or TypeError is raised.
Most Python developers rarely need custom metaclasses — class decorators, __init_subclass__, and descriptors cover most use cases with less complexity. Use metaclasses only when you need to intervene in class creation itself.
💻 Code Example
1# ============================================================2# Understanding type() as a class factory3# ============================================================4# Creating a class dynamically with type()5def greet(self):6 return f"Hello, I'm {self.name}"78Person = type("Person", (), {9 "species": "Homo sapiens",10 "__init__": lambda self, name: setattr(self, "name", name),11 "greet": greet,12 "__repr__": lambda self: f"Person({self.name!r})",13})1415p = Person("Alice")16print(p.greet()) # Hello, I'm Alice17print(type(Person)) # <class 'type'>181920# ============================================================21# Basic metaclass with __new__22# ============================================================23class RegistryMeta(type):24 """Metaclass that auto-registers all classes in a registry."""2526 _registry: dict[str, type] = {}2728 def __new__(mcs, name, bases, namespace):29 cls = super().__new__(mcs, name, bases, namespace)30 # Don't register the base class itself31 if bases:32 mcs._registry[name] = cls33 return cls3435 @classmethod36 def get_registry(mcs) -> dict[str, type]:37 return dict(mcs._registry)383940class Plugin(metaclass=RegistryMeta):41 """Base class for all plugins."""42 def execute(self):43 raise NotImplementedError444546class AuthPlugin(Plugin):47 def execute(self):48 return "Authenticating..."495051class CachePlugin(Plugin):52 def execute(self):53 return "Caching..."545556class LogPlugin(Plugin):57 def execute(self):58 return "Logging..."596061# All subclasses are automatically registered62print(RegistryMeta.get_registry())63# {'AuthPlugin': <class 'AuthPlugin'>, 'CachePlugin': ..., 'LogPlugin': ...}6465# Instantiate by name (useful for config-driven architectures)66plugin_name = "AuthPlugin"67plugin_cls = RegistryMeta.get_registry()[plugin_name]68plugin = plugin_cls()69print(plugin.execute()) # "Authenticating..."707172# ============================================================73# Metaclass with __init__ for validation74# ============================================================75class ValidatedMeta(type):76 """Metaclass that enforces coding contracts on classes."""7778 def __init__(cls, name, bases, namespace):79 super().__init__(name, bases, namespace)8081 # Skip validation for the base class82 if not bases:83 return8485 # Enforce: all concrete classes must have a docstring86 if not cls.__doc__:87 raise TypeError(f"Class {name} must have a docstring")8889 # Enforce: all concrete classes must define 'validate' method90 if "validate" not in namespace and not any(91 hasattr(base, "validate") for base in bases92 ):93 raise TypeError(f"Class {name} must implement validate()")949596class Model(metaclass=ValidatedMeta):97 """Base model class."""98 def validate(self):99 pass100101102class UserModel(Model):103 """User data model with validation."""104105 def __init__(self, name: str, email: str):106 self.name = name107 self.email = email108109 def validate(self):110 if not self.name:111 raise ValueError("Name is required")112 if "@" not in self.email:113 raise ValueError("Invalid email")114 return True115116117# This would raise TypeError:118# class BadModel(Model):119# pass # No docstring! -> TypeError120121122# ============================================================123# Metaclass __call__ — controlling instance creation124# ============================================================125class SingletonMeta(type):126 """Metaclass that makes classes singletons."""127128 _instances: dict[type, object] = {}129130 def __call__(cls, *args, **kwargs):131 if cls not in cls._instances:132 # Create the instance using type.__call__133 instance = super().__call__(*args, **kwargs)134 cls._instances[cls] = instance135 return cls._instances[cls]136137138class AppConfig(metaclass=SingletonMeta):139 def __init__(self, env: str = "production"):140 self.env = env141 self.settings = {}142143 def set(self, key: str, value) -> None:144 self.settings[key] = value145146 def get(self, key: str, default=None):147 return self.settings.get(key, default)148149150config1 = AppConfig("production")151config2 = AppConfig("staging") # Ignored! Returns existing instance152print(config1 is config2) # True153print(config2.env) # "production" (not "staging")154155156# ============================================================157# __init_subclass__ — the simpler alternative to metaclasses158# ============================================================159class Serializable:160 """Base class that tracks all serializable subclasses."""161162 _serializers: dict[str, type] = {}163164 def __init_subclass__(cls, format_name: str = None, **kwargs):165 super().__init_subclass__(**kwargs)166 if format_name:167 cls._format = format_name168 Serializable._serializers[format_name] = cls169170 @classmethod171 def get_serializer(cls, format_name: str):172 return cls._serializers.get(format_name)173174175class JSONSerializer(Serializable, format_name="json"):176 def serialize(self, data) -> str:177 import json178 return json.dumps(data)179180181class CSVSerializer(Serializable, format_name="csv"):182 def serialize(self, data) -> str:183 return ",".join(str(v) for v in data)184185186print(Serializable._serializers)187# {'json': <class 'JSONSerializer'>, 'csv': <class 'CSVSerializer'>}188189serializer = Serializable.get_serializer("json")()190print(serializer.serialize({"key": "value"})) # '{"key": "value"}'191192193# ============================================================194# ABCMeta — abstract base classes195# ============================================================196from abc import ABCMeta, abstractmethod, ABC197198199class Repository(ABC):200 """Abstract repository interface."""201202 @abstractmethod203 def find_by_id(self, id: str):204 """Retrieve an entity by ID."""205 ...206207 @abstractmethod208 def save(self, entity) -> None:209 """Persist an entity."""210 ...211212 @abstractmethod213 def delete(self, id: str) -> bool:214 """Delete an entity by ID."""215 ...216217 def find_all(self) -> list:218 """Default implementation — subclasses may override."""219 return []220221222class InMemoryUserRepo(Repository):223 """Concrete implementation using in-memory storage."""224225 def __init__(self):226 self._store: dict[str, dict] = {}227228 def find_by_id(self, id: str):229 return self._store.get(id)230231 def save(self, entity) -> None:232 self._store[entity["id"]] = entity233234 def delete(self, id: str) -> bool:235 return self._store.pop(id, None) is not None236237238# Repository() # TypeError: Can't instantiate abstract class239repo = InMemoryUserRepo()240repo.save({"id": "1", "name": "Alice"})241print(repo.find_by_id("1")) # {'id': '1', 'name': 'Alice'}242243244# ============================================================245# Combining metaclass features with __prepare__246# ============================================================247from collections import OrderedDict248249250class OrderedMeta(type):251 """Metaclass that preserves attribute definition order."""252253 @classmethod254 def __prepare__(mcs, name, bases):255 # Return an OrderedDict so attribute order is tracked256 return OrderedDict()257258 def __new__(mcs, name, bases, namespace):259 cls = super().__new__(mcs, name, bases, dict(namespace))260 cls._field_order = [261 key for key in namespace262 if not key.startswith("_") and not callable(namespace[key])263 ]264 return cls265266267class FormFields(metaclass=OrderedMeta):268 username = "text"269 email = "email"270 password = "password"271 confirm = "password"272273print(FormFields._field_order)274# ['username', 'email', 'password', 'confirm']
🏋️ Practice Exercise
Exercises:
Create a
RegistryMetametaclass that maintains a dictionary of all classes created with it. Add a class methodcreate(name, **kwargs)to the base class that looks up the registry and instantiates the correct subclass by name.Build a
ValidatedModelMetametaclass that inspects type annotations in class bodies and automatically generates__init__andvalidatemethods. For example,name: strshould ensurenameis a string invalidate().Implement the Singleton pattern using: (a) a metaclass, (b) a class decorator, and (c)
__new__. Compare the three approaches in terms of inheritance behavior, thread safety, and debuggability.Rewrite the
RegistryMetaexample using__init_subclass__instead of a metaclass. Discuss when__init_subclass__is sufficient and when you genuinely need a metaclass.Create an abstract base class
ShapewithABCMetathat requiresarea()andperimeter()methods. Add a concrete methoddescription()that uses both. ImplementCircle,Rectangle, andTrianglesubclasses and verify that incomplete implementations raiseTypeError.Write a metaclass that automatically wraps all methods of a class with a timing decorator, but skips dunder methods. Test it with a class that has 5+ methods.
⚠️ Common Mistakes
Using metaclasses when simpler alternatives exist.
__init_subclass__, class decorators, and descriptors solve most class-customization problems without metaclass complexity. Reach for metaclasses only when you need to control the class creation process itself (e.g., modifying__prepare__, intercepting__new__before the class object exists).Confusing
__new__and__init__in metaclasses. In a metaclass,__new__creates the class object (not an instance), and__init__initializes it after creation. In regular classes,__new__creates the instance. Themcs/clsparameter naming reflects this:mcs= metaclass,cls= the class being created.Metaclass conflicts when using multiple inheritance. If
Parent1usesMetaAandParent2usesMetaB,class Child(Parent1, Parent2)raisesTypeErrorunless one meta is a subclass of the other. Fix by creating a combined metaclass:class CombinedMeta(MetaA, MetaB): pass.Forgetting that
__init_subclass__runs at class creation time, not at instantiation time. Side effects in__init_subclass__(like registering plugins, modifying class attributes) happen when Python reads the class definition, which can cause issues with import order and circular dependencies.Not calling
super().__new__()orsuper().__init__()in metaclass methods. Skipping the super call meanstype's default behavior is bypassed, potentially creating malformed class objects that fail in subtle ways.
💼 Interview Questions
🎤 Mock Interview
Practice a live interview for Metaclasses