🎯 Learning Objectives
- Explain the type hierarchy:
typeis both a class and its own metaclass; every class is an instance oftype - Trace the class creation protocol step-by-step:
__prepare__,__new__,__init__ - Write a custom metaclass that enforces structural rules (abstract methods, naming conventions, required attributes)
- Use
__init_subclass__as a lightweight alternative to metaclasses for subclass hooks - Use
__class_getitem__to implement generic-style syntax (MyClass[T]) - Build a plugin/registry system using metaclass hooks
- Understand the metaclass conflict problem and resolution with
__mro_entries__
1 · Everything is an Instance of type
In Python, classes are first-class objects — instances of type. This is the foundation of the entire metaclass system. Every class you define (or that ships with the standard library) is literally an object whose type is type.
# Every class is an instance of type
print(type(int)) # <class 'type'>
print(type(str)) # <class 'type'>
print(type(list)) # <class 'type'>
class MyClass:
pass
print(type(MyClass)) # <class 'type'>
print(isinstance(MyClass, type)) # True
# type is its own metaclass
print(type(type)) # <class 'type'>
# The type hierarchy
print(issubclass(type, object)) # True
print(issubclass(object, type)) # False
print(isinstance(type, object)) # True — type is an instance of objecttype_hierarchy.pyThe relationship between type and object forms a curious loop: type is a subclass of object, but object is an instance of type. This bootstrap cycle is hard-wired into the CPython interpreter at the C level.
┌─────────────────────────────────────────────┐ │ instance-of (type) subclass-of (is-a)│ ├─────────────────────────────────────────────┤ │ │ │ int ──instance-of──▶ type │ │ str ──instance-of──▶ type │ │ MyClass ─instance-of──▶ type │ │ type ──instance-of──▶ type (self-ref!) │ │ │ │ int ──subclass-of──▶ object │ │ str ──subclass-of──▶ object │ │ type ──subclass-of──▶ object │ │ object ──instance-of──▶ type │ └─────────────────────────────────────────────┘
Because type is itself callable, you can use it directly to create classes dynamically — no class statement needed:
# type(name, bases, namespace) creates a new class
MyDynamic = type("MyDynamic", (object,), {
"x": 42,
"greet": lambda self: f"Hello from {type(self).__name__}",
})
obj = MyDynamic()
print(obj.greet()) # Hello from MyDynamic
print(obj.x) # 42
print(type(MyDynamic)) # <class 'type'>
# This is EXACTLY what the `class` statement does internallydynamic_class.pyclass statement is syntactic sugar for a type() call (or a custom metaclass call). When Python sees class Foo(Bar):, it: (1) collects the class body namespace, (2) determines the metaclass, (3) calls metaclass(name, bases, namespace). The default metaclass is type.2 · The Class Creation Protocol
When Python executes a class statement, the following sequence runs internally. Understanding this protocol is essential for writing correct metaclasses:
class Foo(Base, metaclass=Meta):
body...
Internally:
1. Determine metaclass:
meta = Meta (explicit) or type(Base) or type
2. Prepare namespace:
namespace = meta.__prepare__("Foo", (Base,), **kwargs)
# Returns a dict (or custom mapping) used to execute the class body
3. Execute class body:
exec(body, globals, namespace)
# All assignments in the body land in `namespace`
4. Create the class object:
Foo = meta.__new__(meta, "Foo", (Base,), namespace)
5. Initialise the class object:
meta.__init__(Foo, "Foo", (Base,), namespace)
6. Call __set_name__ on all descriptors in namespace
7. Call Base.__init_subclass__(cls=Foo, **kwargs) on each base
We can observe every step by writing a tracing metaclass:
class TracingMeta(type):
@classmethod
def __prepare__(mcs, name, bases, **kwargs):
print(f" __prepare__({name!r}, {[b.__name__ for b in bases]})")
return super().__prepare__(name, bases, **kwargs) # returns a dict
def __new__(mcs, name, bases, namespace, **kwargs):
print(f" __new__({name!r}, attrs={list(namespace.keys())})")
return super().__new__(mcs, name, bases, namespace)
def __init__(cls, name, bases, namespace, **kwargs):
print(f" __init__({name!r})")
super().__init__(name, bases, namespace)
class Base(metaclass=TracingMeta):
x = 1
# Output:
# __prepare__('Base', [])
# __new__('Base', attrs=['__module__', '__qualname__', 'x'])
# __init__('Base')
class Child(Base):
y = 2
# Output:
# __prepare__('Child', ['Base'])
# __new__('Child', attrs=['__module__', '__qualname__', 'y'])
# __init__('Child')tracing_meta.py__prepare__ matters: It is called before the class body executes and returns the namespace dict that will be populated. This is how OrderedDict-based metaclasses preserve field declaration order — and how the enum module tracks insertion order of members.3 · Writing a Metaclass
A practical metaclass enforces structural rules at class-creation time — before any instance is ever created. This moves many categories of bugs from runtime to definition time:
import re
class InterfaceMeta(type):
"""
Enforces that every concrete (non-abstract) subclass:
1. Implements all methods listed in `__required_methods__`
2. Uses snake_case names for public methods
3. Has a docstring
"""
_SNAKE = re.compile(r'^[a-z_][a-z0-9_]*$')
def __new__(mcs, name, bases, namespace, **kwargs):
cls = super().__new__(mcs, name, bases, namespace)
# Skip the base class itself
if not bases:
return cls
required = set()
for base in bases:
required.update(getattr(base, '__required_methods__', []))
# Check all required methods are implemented
for method_name in required:
if not callable(getattr(cls, method_name, None)):
raise TypeError(
f"Class '{name}' must implement '{method_name}()'"
)
# Enforce snake_case on public methods
for attr, val in namespace.items():
if callable(val) and not attr.startswith('_'):
if not mcs._SNAKE.match(attr):
raise NameError(
f"Method '{attr}' in '{name}' must be snake_case"
)
# Enforce docstring
if not cls.__doc__:
raise TypeError(f"Class '{name}' must have a docstring")
return cls
class Storage(metaclass=InterfaceMeta):
"""Abstract base for storage backends."""
__required_methods__ = ["read", "write", "delete"]
class FileStorage(Storage):
"""File-based storage backend."""
def read(self, key: str) -> bytes:
"""Read bytes from file."""
return open(key, "rb").read()
def write(self, key: str, data: bytes) -> None:
"""Write bytes to file."""
open(key, "wb").write(data)
def delete(self, key: str) -> None:
"""Delete a file."""
import os; os.unlink(key)
# This will raise TypeError — missing `delete`
try:
class BadStorage(Storage):
"""Missing delete method."""
def read(self, key): pass
def write(self, key, data): pass
except TypeError as e:
print(e) # Class 'BadStorage' must implement 'delete()'interface_meta.py__new__ (not __init__) when you need to modify or reject the class before it fully exists. Use __init__ for post-creation setup that doesn't need to alter the class object itself.4 · __init_subclass__: The Lightweight Alternative
Python 3.6+ provides __init_subclass__ — called on the parent class each time it is subclassed. It handles 90% of metaclass use cases without the complexity of writing a full metaclass.
class PluginBase:
"""
Subclasses automatically register themselves in _registry
via __init_subclass__.
"""
_registry: dict[str, type] = {}
def __init_subclass__(cls, plugin_name: str = None, **kwargs):
super().__init_subclass__(**kwargs) # always call super()
name = plugin_name or cls.__name__.lower()
if name in PluginBase._registry:
raise ValueError(f"Plugin '{name}' already registered")
PluginBase._registry[name] = cls
print(f" Registered plugin: '{name}' → {cls.__name__}")
class JSONPlugin(PluginBase, plugin_name="json"):
def process(self, data): return f"JSON: {data}"
class XMLPlugin(PluginBase, plugin_name="xml"):
def process(self, data): return f"XML: {data}"
class CSVPlugin(PluginBase): # uses class name as key: 'csvplugin'
def process(self, data): return f"CSV: {data}"
print(PluginBase._registry)
# {'json': JSONPlugin, 'xml': XMLPlugin, 'csvplugin': CSVPlugin}
plugin = PluginBase._registry["json"]()
print(plugin.process({"key": "value"}))plugin_registry.pyNotice how __init_subclass__ receives keyword arguments directly from the class statement — no metaclass machinery required:
class Validator(PluginBase, plugin_name="validate"):
"""Receives plugin_name='validate' from the class statement."""
def process(self, data): return f"VALID: {data}"plugin_kwargs.py__init_subclass__? It is simpler than metaclasses because: it lives on the class itself (no separate metaclass hierarchy), it receives keyword arguments from the class statement, it always calls super().__init_subclass__(**kwargs) to propagate to other classes in the MRO, and it avoids metaclass conflict issues entirely.5 · __class_getitem__ for Generic Syntax
__class_getitem__ enables MyClass[T] syntax without importing anything from typing. This is the mechanism that powers list[int] and dict[str, int] in Python 3.9+:
from __future__ import annotations
from typing import TypeVar, Generic
T = TypeVar("T")
# ── Standard library Generic uses __class_getitem__ ──
# list[int], dict[str, int] work because list.__class_getitem__ returns a GenericAlias
print(list[int]) # list[int] — a types.GenericAlias
print(dict[str, int]) # dict[str, int]
# ── Custom generic class ──
class Stack:
"""A typed stack using __class_getitem__."""
def __class_getitem__(cls, item):
"""Called when Stack[int] is written."""
# Return a GenericAlias — allows Stack[int] as a type hint
import types
return types.GenericAlias(cls, (item,))
def __init__(self):
self._data: list = []
def push(self, item) -> None:
self._data.append(item)
def pop(self):
return self._data.pop()
def __repr__(self):
return f"Stack({self._data!r})"
# Now Stack[int] is valid in type hints
def process(stack: Stack[int]) -> int:
return stack.pop()
s: Stack[int] = Stack()
s.push(1); s.push(2)
print(process(s)) # 2
print(Stack[int]) # stack.Stack[int]class_getitem_basic.pyFor more advanced use cases, __class_getitem__ can return a dynamically-created class that carries metadata about the type parameter:
# ── Metadata-carrying generic alias ──
class TypedCollection:
_item_type: type = object
def __class_getitem__(cls, item_type):
new_cls = type(
f"{cls.__name__}[{item_type.__name__}]",
(cls,),
{"_item_type": item_type},
)
return new_cls
def append(self, item):
if not isinstance(item, type(self)._item_type):
raise TypeError(
f"Expected {type(self)._item_type.__name__}, "
f"got {type(item).__name__}"
)
self._items = getattr(self, "_items", [])
self._items.append(item)
IntCollection = TypedCollection[int]
c = IntCollection()
c.append(42)
try:
c.append("hello")
except TypeError as e:
print(e) # Expected int, got strtyped_collection.py6 · __prepare__ and Ordered Namespaces
__prepare__ lets you substitute the default dict namespace with a custom mapping before the class body executes. This is how enum.EnumType tracks member insertion order:
from collections import OrderedDict
class OrderedMeta(type):
"""Metaclass that preserves declaration order of all attributes."""
@classmethod
def __prepare__(mcs, name, bases, **kwargs):
return OrderedDict() # class body written into an OrderedDict
def __new__(mcs, name, bases, namespace, **kwargs):
# namespace is the OrderedDict we returned from __prepare__
cls = super().__new__(mcs, name, bases, dict(namespace))
cls._field_order = [
k for k in namespace
if not k.startswith("__") and not callable(namespace[k])
]
return cls
class Schema(metaclass=OrderedMeta):
"""Preserves field declaration order."""
first_name: str = ""
last_name: str = ""
age: int = 0
email: str = ""
def full_name(self):
return f"{self.first_name} {self.last_name}"
print(Schema._field_order)
# ['first_name', 'last_name', 'age', 'email'] — guaranteed orderordered_meta.pyA more powerful pattern: intercept every attribute assignment during class body execution using a custom namespace that overrides __setitem__:
# ── Counting class: use a custom namespace to auto-number fields ──
class NumberedNamespace(dict):
def __init__(self):
super().__init__()
self._counter = 0
def __setitem__(self, key, value):
if not key.startswith("_") and isinstance(value, str) and value == "auto":
value = self._counter
self._counter += 1
super().__setitem__(key, value)
class NumberedMeta(type):
@classmethod
def __prepare__(mcs, name, bases, **kwargs):
return NumberedNamespace()
class Fields(metaclass=NumberedMeta):
first = "auto"
second = "auto"
third = "auto"
print(Fields.first, Fields.second, Fields.third) # 0 1 2numbered_namespace.pydict became ordered). __prepare__ is now mainly needed when you want a custom mapping type — e.g. to intercept attribute assignments during class body execution (as shown in the NumberedNamespace example above).7 · Metaclass Conflict & __mro_entries__
When inheriting from two classes with different metaclasses, Python raises a TypeError. This is the infamous "metaclass conflict" — the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases:
class MetaA(type):
def __new__(mcs, name, bases, ns):
print(f"MetaA creating {name}")
return super().__new__(mcs, name, bases, ns)
class MetaB(type):
def __new__(mcs, name, bases, ns):
print(f"MetaB creating {name}")
return super().__new__(mcs, name, bases, ns)
class A(metaclass=MetaA): pass
class B(metaclass=MetaB): pass
# This raises: TypeError: metaclass conflict: the metaclass of a derived class
# must be a (non-strict) subclass of the metaclasses of all its bases
try:
class C(A, B): pass
except TypeError as e:
print(e)
# Fix: create a combined metaclass
class MetaAB(MetaA, MetaB): pass # inherits from both
class C(A, B, metaclass=MetaAB): pass # now worksmetaclass_conflict.py__mro_entries__ is a lesser-known protocol that lets non-class objects appear in base class lists. When Python encounters an object in the bases tuple that is not a class, it calls obj.__mro_entries__(bases) to get the actual classes to insert into the MRO:
class GenericBase:
"""Supports MyClass[T] in inheritance: class Foo(GenericBase[int])."""
def __class_getitem__(cls, params):
class _GenericAlias:
def __mro_entries__(self, bases):
# Replace this alias with the actual class in the MRO
return (cls,)
def __repr__(self):
return f"{cls.__name__}[{params!r}]"
return _GenericAlias()
class IntList(GenericBase[int]):
"""MRO resolves to: IntList → GenericBase → object."""
pass
print(IntList.__bases__) # (<class 'GenericBase'>,)
print(IntList.__mro__) # (IntList, GenericBase, object)mro_entries.py__init_subclass__, class decorators, or __set_name__ cannot solve the problem. The bar is: "Does this need to run at class creation time and affect the class object itself?" If the answer is yes, and simpler hooks won't do, then a metaclass is justified.Singleton & Borg Patterns via Metaclass
Metaclasses are the cleanest way to implement the Singleton pattern in Python —
no module-level tricks or __new__ hacks needed.
class SingletonMeta(type):
"""Metaclass that ensures only one instance per class."""
_instances: dict = {}
def __call__(cls, *args, **kwargs):
# __call__ on the metaclass intercepts instance creation
if cls not in cls._instances:
# super().__call__ runs cls.__new__ then cls.__init__
cls._instances[cls] = super().__call__(*args, **kwargs)
return cls._instances[cls]
class Config(metaclass=SingletonMeta):
def __init__(self, debug: bool = False):
self.debug = debug
c1 = Config(debug=True)
c2 = Config()
print(c1 is c2) # True — same object
print(c2.debug) # True — init only ran once
# Reset for testing
del SingletonMeta._instances[Config]
c3 = Config(debug=False)
print(c3 is c1) # False — fresh instance
class BorgMeta(type):
"""
Borg pattern: every instance is distinct but shares __dict__.
All instances share state without being the same object.
"""
def __new__(mcs, name, bases, namespace):
cls = super().__new__(mcs, name, bases, namespace)
cls._shared_state: dict = {}
return cls
def __call__(cls, *args, **kwargs):
instance = cls.__new__(cls, *args, **kwargs)
instance.__dict__ = cls._shared_state # share the dict
instance.__init__(*args, **kwargs)
return instance
class AppState(metaclass=BorgMeta):
def __init__(self, **kw):
self.__dict__.update(kw)
s1 = AppState(x=1)
s2 = AppState(y=2)
print(s1 is s2) # False — distinct objects
print(s1.y) # 2 — shared state
print(s2.x) # 1 — shared state
singleton_borg.py
sys.modules,
so module-level state is effectively a singleton. Reach for SingletonMeta
only when you need multiple distinct singleton classes or testability via
_instances reset.
Abstract Base Classes Without abc
The abc module is itself built on metaclasses. Here is how to
build the same mechanism from first principles — and then how to use
abc.ABCMeta properly.
class AbstractMeta(type):
"""
Marks methods decorated with @abstract as requiring implementation.
Raises TypeError when an abstract class is instantiated directly.
"""
def __new__(mcs, name, bases, namespace):
abstract_methods = set()
# Collect from bases
for base in bases:
abstract_methods.update(getattr(base, "_abstract_methods", set()))
# Add newly marked abstracts, remove ones that are implemented
for k, v in namespace.items():
if getattr(v, "_abstract", False):
abstract_methods.add(k)
elif k in abstract_methods:
abstract_methods.discard(k)
cls = super().__new__(mcs, name, bases, namespace)
cls._abstract_methods = frozenset(abstract_methods)
return cls
def __call__(cls, *args, **kwargs):
if cls._abstract_methods:
raise TypeError(
f"Cannot instantiate abstract class '{cls.__name__}' — "
f"unimplemented: {sorted(cls._abstract_methods)}"
)
return super().__call__(*args, **kwargs)
def abstract(fn):
"""Decorator to mark a method as abstract."""
fn._abstract = True
return fn
class Shape(metaclass=AbstractMeta):
@abstract
def area(self) -> float: ...
@abstract
def perimeter(self) -> float: ...
def describe(self) -> str:
return f"{type(self).__name__}: area={self.area():.2f}"
class Circle(Shape):
def __init__(self, r: float): self.r = r
def area(self) -> float: import math; return math.pi * self.r ** 2
def perimeter(self) -> float: import math; return 2 * math.pi * self.r
c = Circle(5)
print(c.describe()) # Circle: area=78.54
try:
Shape()
except TypeError as e:
print(e) # Cannot instantiate abstract class 'Shape' — unimplemented: ['area', 'perimeter']
# ── Real ABCMeta (stdlib) ──
from abc import ABCMeta, abstractmethod
class Repository(metaclass=ABCMeta):
@abstractmethod
def find(self, id: int): ...
@abstractmethod
def save(self, entity) -> None: ...
def find_all(self) -> list: # concrete method
return []
abstract_meta.py
Real-World Example: Django-Style Model Metaclass
Django's ModelBase metaclass is one of the most studied metaclasses
in Python. Here is a stripped-down version showing the core techniques:
class FieldDescriptor:
"""Data descriptor for a model field."""
def __set_name__(self, owner, name):
self.name = name
self.private = f"_{name}"
def __init__(self, field_type: type, **opts):
self.field_type = field_type
self.opts = opts
def __get__(self, obj, objtype=None):
if obj is None: return self
return getattr(obj, self.private, None)
def __set__(self, obj, value):
if value is not None and not isinstance(value, self.field_type):
raise TypeError(f"Field '{self.name}': expected {self.field_type.__name__}")
setattr(obj, self.private, value)
class ModelMeta(type):
"""
Metaclass that:
1. Collects all FieldDescriptor declarations into _meta.fields
2. Injects a primary key if none is declared
3. Generates __repr__ automatically
4. Provides a classmethod .objects.all() stub
"""
def __new__(mcs, name, bases, namespace, **kwargs):
fields = {}
# Inherit fields from base classes
for base in bases:
fields.update(getattr(base, "_fields", {}))
# Collect FieldDescriptor instances from current namespace
for k, v in list(namespace.items()):
if isinstance(v, FieldDescriptor):
fields[k] = v
# Auto-inject 'id' primary key if not declared
if "id" not in fields and name != "Model":
id_field = FieldDescriptor(int, primary_key=True)
id_field.name = "id"
id_field.private = "_id"
namespace["id"] = id_field
fields["id"] = id_field
namespace["_fields"] = fields
cls = super().__new__(mcs, name, bases, namespace)
# Inject __repr__
def _repr(self):
parts = ", ".join(
f"{k}={getattr(self, k)!r}"
for k in self._fields
if getattr(self, k) is not None
)
return f"{type(self).__name__}({parts})"
cls.__repr__ = _repr
return cls
class Model(metaclass=ModelMeta):
"""Base model class."""
def __init__(self, **kwargs):
for k, v in kwargs.items():
setattr(self, k, v)
class User(Model):
username = FieldDescriptor(str)
email = FieldDescriptor(str)
age = FieldDescriptor(int)
class Post(Model):
title = FieldDescriptor(str)
body = FieldDescriptor(str)
user_id = FieldDescriptor(int)
u = User(username="alice", email="alice@example.com", age=30)
print(u) # User(id=None, username='alice', email='alice@example.com', age=30)
p = Post(title="Hello", body="World", user_id=1)
print(p) # Post(id=None, title='Hello', body='World', user_id=1)
print(User._fields.keys()) # dict_keys(['id', 'username', 'email', 'age'])
django_style.py
ModelBase works: it iterates the
namespace collecting Field objects, builds _meta.fields,
auto-injects pk, and contributes each field to the class.
SQLAlchemy's DeclarativeMeta uses the same pattern to collect
Column objects and map them to database columns.
Best Practices
- Prefer
__init_subclass__over metaclasses for most subclass hooks — it is simpler, avoids metaclass conflicts, and is easier to test. Only use a metaclass when you need to intercept__prepare__, modify the class object itself in__new__, or hook into__call__(instance creation). - Always call
super()in every metaclass hook —super().__prepare__,super().__new__,super().__init__— to stay cooperative with other metaclasses in the chain. - Always call
super().__init_subclass__(**kwargs)and forward all unused kwargs — this enables cooperative multiple inheritance with other__init_subclass__hooks higher in the MRO. - Use
__set_name__instead of metaclass field-collection when descriptors just need their own name —__set_name__is called automatically and needs no metaclass at all. - Resolve metaclass conflicts early — create a combined metaclass (
class CombinedMeta(MetaA, MetaB): pass) and pass it explicitly rather than letting Python raiseTypeErrorat class creation time. - Document metaclass behaviour prominently — metaclasses work at class-creation time, which is non-obvious. A clear docstring explaining what the metaclass injects or enforces is essential for maintainability.
- Use
type.__new__carefully — do all structural modifications (adding methods, registering fields) in__new__, not__init__, because__new__receives the namespace before descriptors run__set_name__. - Test metaclass behaviour with minimal classes — write unit tests that create tiny classes using the metaclass and assert on their structure; do not rely on integration tests to catch metaclass bugs.
Exercises
Exercise 1 — Registry Metaclass
Build a HandlerMeta metaclass that automatically registers
subclasses in a central registry, keyed by a command class
attribute:
- The metaclass maintains a class-level
_registry: dict[str, type]. - When any subclass of
BaseHandleris created, if it has acommandattribute, register it under that key. - Provide a
get_handler(command: str) -> BaseHandlerclassmethod that looks up and instantiates the handler. - Raise
KeyErrorwith a helpful message if the command is not registered. - Test: define three handler subclasses; verify all three are in the registry; verify
get_handler("list")()returns the right instance.
💡 Hint
class HandlerMeta(type):
_registry: dict[str, type] = {}
def __new__(mcs, name, bases, namespace, **kwargs):
cls = super().__new__(mcs, name, bases, namespace)
if "command" in namespace:
cmd = namespace["command"]
if cmd in mcs._registry:
raise ValueError(f"Command '{cmd}' already registered")
mcs._registry[cmd] = cls
return cls
class BaseHandler(metaclass=HandlerMeta):
@classmethod
def get_handler(cls, command: str) -> "BaseHandler":
try:
return HandlerMeta._registry[command]()
except KeyError:
available = sorted(HandlerMeta._registry.keys())
raise KeyError(f"Unknown command {command!r}. Available: {available}")
class ListHandler(BaseHandler):
command = "list"
def run(self): return "listing items"
class CreateHandler(BaseHandler):
command = "create"
def run(self): return "creating item"
h = BaseHandler.get_handler("list")
print(h.run()) # listing items
Exercise 2 — Declarative Schema with __prepare__
Build a SchemaMeta metaclass that uses a custom
__prepare__ namespace to track field declaration order
and auto-generate __init__, __repr__, and
validate():
- The custom namespace records the order that
Fieldobjects are assigned. __init__accepts keyword arguments for every declaredField.validate()checks that all non-optional fields have values.- Test with a
UserSchemahavingname(required) andage(optional, default=None).
💡 Hint
from collections import OrderedDict
class Field:
def __init__(self, typ, *, required=True, default=None):
self.typ = typ
self.required = required
self.default = default
class FieldOrderNamespace(OrderedDict):
def __init__(self):
super().__init__()
self._fields = OrderedDict()
def __setitem__(self, k, v):
if isinstance(v, Field):
self._fields[k] = v
super().__setitem__(k, v)
class SchemaMeta(type):
@classmethod
def __prepare__(mcs, name, bases, **kw):
return FieldOrderNamespace()
def __new__(mcs, name, bases, ns, **kw):
fields = dict(ns._fields)
cls = super().__new__(mcs, name, bases, dict(ns))
cls._schema_fields = fields
def __init__(self, **kwargs):
for fname, field in fields.items():
setattr(self, fname, kwargs.get(fname, field.default))
def __repr__(self):
parts = ", ".join(f"{k}={getattr(self,k)!r}" for k in fields)
return f"{type(self).__name__}({parts})"
def validate(self):
for fname, field in fields.items():
if field.required and getattr(self, fname) is None:
raise ValueError(f"Field '{fname}' is required")
cls.__init__ = __init__
cls.__repr__ = __repr__
cls.validate = validate
return cls
class Schema(metaclass=SchemaMeta): pass
class UserSchema(Schema):
name = Field(str, required=True)
age = Field(int, required=False, default=None)
u = UserSchema(name="Alice", age=30)
print(u) # UserSchema(name='Alice', age=30)
u.validate() # passes
u2 = UserSchema()
try:
u2.validate()
except ValueError as e:
print(e) # Field 'name' is required
Exercise 3 — Metaclass vs __init_subclass__ Comparison
Implement the same interface-enforcement system two ways and compare:
- Version A — Metaclass:
InterfaceMetathat checks for required methods in__new__and raisesTypeErrorif any are missing. - Version B —
__init_subclass__:Interfacebase class that uses__init_subclass__to enforce the same check. - Both versions must enforce: all methods in
__required__ = [...]are implemented in concrete subclasses. - Write identical test cases for both versions: valid subclass passes; missing-method subclass raises
TypeError. - Write a brief comparison: which is simpler? Which handles multiple inheritance more cleanly?
💡 Hint — Version B
class Interface:
__required__: list[str] = []
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
required = set()
for base in cls.__mro__:
required.update(getattr(base, "__required__", []))
missing = [m for m in required if not callable(getattr(cls, m, None))]
# Only enforce on concrete classes (no further required methods)
if missing and not getattr(cls, "__abstract__", False):
raise TypeError(f"{cls.__name__} must implement: {missing}")
class DataStore(Interface):
__required__ = ["read", "write"]
__abstract__ = True # skip check for this abstract class
class FileStore(DataStore):
def read(self, k): return b""
def write(self, k, v): pass # passes — both methods present
try:
class BadStore(DataStore): pass # raises TypeError
except TypeError as e:
print(e)