Metaclasses
Metaclass and descriptor patterns for advanced class customization in Python
You are an expert in Python metaclasses and descriptors for advanced class creation and attribute management. ## Key Points - **A metaclass** is a class whose instances are classes. The default metaclass is `type`. - **`__init_subclass__`** (Python 3.6+) is a simpler hook that runs when a class is subclassed — often a better alternative to metaclasses. - **Descriptors** implement `__get__`, `__set__`, and/or `__delete__` to customize attribute access. - **Data descriptors** define `__set__` or `__delete__` and take priority over instance `__dict__`. - **Non-data descriptors** define only `__get__` (e.g., functions/methods) and yield to instance `__dict__`. - **`__set_name__`** (Python 3.6+) is called on descriptors when the owning class is created, providing the attribute name. - **Prefer `__init_subclass__`** over metaclasses for registration, validation, and simple class customization — it is easier to understand and compose. - Use descriptors to encapsulate reusable attribute logic (validation, lazy computation, type coercion). - Always implement `__set_name__` on descriptors to automatically capture the attribute name. - Keep metaclass logic minimal — they are hard to debug and compose (a class can only have one metaclass lineage). - Document metaclass behavior explicitly; users of your framework need to know what magic is happening. - **Metaclass conflicts** — if two base classes have different metaclasses that are not related by inheritance, Python raises `TypeError`.
skilldb get python-patterns-skills/MetaclassesFull skill: 225 linesMetaclasses — Python Patterns
You are an expert in Python metaclasses and descriptors for advanced class creation and attribute management.
Overview
Metaclasses control how classes themselves are created — they are "classes of classes." Descriptors control how attribute access works on instances. Together, they are the machinery behind property, classmethod, staticmethod, ORMs, and many frameworks. Most Python developers rarely need to write metaclasses directly, but understanding them is essential for framework design and debugging.
Core Philosophy
Metaclasses and descriptors are Python's deepest customization hooks — they let you change what it means to define a class or access an attribute. This power is appropriate for framework authors who need to enforce contracts, register classes automatically, or transform class definitions at creation time. For application developers, the right question is almost always "can I do this with a simpler mechanism first?" — and the answer is usually yes.
The hierarchy of class customization tools should guide your design decisions. Start with plain classes and functions. If you need to customize subclass behavior, use __init_subclass__. If you need to control attribute access, use descriptors or property. If you need to enforce an interface, consider Protocol or ABC. Only reach for metaclasses when none of these simpler tools suffice — typically when you need to manipulate the class namespace before the class object is created, or when you need to intercept class creation for all subclasses in a hierarchy.
Descriptors deserve special attention because they are the hidden machinery behind so much of Python's behavior. Properties, methods, class methods, static methods, and __slots__ are all implemented as descriptors. When you understand the descriptor protocol — __get__, __set__, __delete__, and __set_name__ — you understand how Python's attribute access really works. This knowledge lets you build reusable, declarative field behaviors (validation, lazy loading, type coercion) that integrate seamlessly with Python's object model.
Core Concepts
- A metaclass is a class whose instances are classes. The default metaclass is
type. __init_subclass__(Python 3.6+) is a simpler hook that runs when a class is subclassed — often a better alternative to metaclasses.- Descriptors implement
__get__,__set__, and/or__delete__to customize attribute access. - Data descriptors define
__set__or__delete__and take priority over instance__dict__. - Non-data descriptors define only
__get__(e.g., functions/methods) and yield to instance__dict__. __set_name__(Python 3.6+) is called on descriptors when the owning class is created, providing the attribute name.
Implementation Patterns
Basic metaclass
class RegistryMeta(type):
registry: dict[str, type] = {}
def __new__(mcs, name, bases, namespace):
cls = super().__new__(mcs, name, bases, namespace)
if name != "Base":
mcs.registry[name] = cls
return cls
class Base(metaclass=RegistryMeta):
pass
class PluginA(Base):
pass
class PluginB(Base):
pass
print(RegistryMeta.registry)
# {"PluginA": <class 'PluginA'>, "PluginB": <class 'PluginB'>}
init_subclass (preferred alternative)
class Plugin:
_registry: dict[str, type] = {}
def __init_subclass__(cls, *, plugin_name: str | None = None, **kwargs):
super().__init_subclass__(**kwargs)
name = plugin_name or cls.__name__
Plugin._registry[name] = cls
class AuthPlugin(Plugin, plugin_name="auth"):
pass
class CachePlugin(Plugin, plugin_name="cache"):
pass
print(Plugin._registry)
# {"auth": <class 'AuthPlugin'>, "cache": <class 'CachePlugin'>}
Enforcing interface with metaclass
class InterfaceMeta(type):
def __new__(mcs, name, bases, namespace):
cls = super().__new__(mcs, name, bases, namespace)
if bases: # skip the base class itself
required = {"execute", "validate"}
missing = required - set(namespace)
if missing:
raise TypeError(
f"{name} must implement: {', '.join(missing)}"
)
return cls
class Command(metaclass=InterfaceMeta):
pass
class SaveCommand(Command):
def execute(self): ...
def validate(self): ...
# Works fine
# class BadCommand(Command):
# def execute(self): ...
# # TypeError: BadCommand must implement: validate
Data descriptor with validation
class Validated:
def __set_name__(self, owner, name):
self.name = name
self.storage_name = f"_{name}"
def __get__(self, obj, objtype=None):
if obj is None:
return self
return getattr(obj, self.storage_name, None)
def __set__(self, obj, value):
self.validate(value)
setattr(obj, self.storage_name, value)
def validate(self, value):
raise NotImplementedError
class PositiveNumber(Validated):
def validate(self, value):
if not isinstance(value, (int, float)):
raise TypeError(f"{self.name} must be a number")
if value <= 0:
raise ValueError(f"{self.name} must be positive")
class BoundedString(Validated):
def __init__(self, max_length: int = 100):
self.max_length = max_length
def validate(self, value):
if not isinstance(value, str):
raise TypeError(f"{self.name} must be a string")
if len(value) > self.max_length:
raise ValueError(f"{self.name} exceeds {self.max_length} chars")
class Product:
name = BoundedString(max_length=50)
price = PositiveNumber()
quantity = PositiveNumber()
p = Product()
p.name = "Widget"
p.price = 9.99
p.quantity = 100
Non-data descriptor (method-like behavior)
class LazyProperty:
"""Descriptor that computes a value once and caches it on the instance."""
def __init__(self, func):
self.func = func
self.__doc__ = func.__doc__
def __set_name__(self, owner, name):
self.name = name
def __get__(self, obj, objtype=None):
if obj is None:
return self
value = self.func(obj)
# Cache on instance dict — bypasses descriptor on next access
setattr(obj, self.name, value)
return value
class DataPipeline:
@LazyProperty
def expensive_result(self):
print("Computing...")
return sum(range(10_000_000))
pipeline = DataPipeline()
print(pipeline.expensive_result) # "Computing..." then result
print(pipeline.expensive_result) # cached, no recomputation
class_getitem for generic-like syntax
class Wrapper:
def __class_getitem__(cls, item):
return type(f"Wrapper[{item.__name__}]", (cls,), {"item_type": item})
IntWrapper = Wrapper[int]
print(IntWrapper.item_type) # <class 'int'>
Best Practices
- Prefer
__init_subclass__over metaclasses for registration, validation, and simple class customization — it is easier to understand and compose. - Use descriptors to encapsulate reusable attribute logic (validation, lazy computation, type coercion).
- Always implement
__set_name__on descriptors to automatically capture the attribute name. - Keep metaclass logic minimal — they are hard to debug and compose (a class can only have one metaclass lineage).
- Document metaclass behavior explicitly; users of your framework need to know what magic is happening.
Common Pitfalls
- Metaclass conflicts — if two base classes have different metaclasses that are not related by inheritance, Python raises
TypeError. - Descriptor on the instance instead of the class — descriptors must be defined as class attributes, not in
__init__. - Forgetting
__set_name__means the descriptor does not know its attribute name and cannot create unique storage per field. - Non-data descriptor vs instance dict — if a value is set in
__dict__, a non-data descriptor is shadowed; data descriptors always win. - Overusing metaclasses when a class decorator,
__init_subclass__, or a simple base class would suffice — metaclasses should be a last resort.
Anti-Patterns
-
Metaclass when init_subclass would suffice — writing a full metaclass with
__new__and__init__to register subclasses or validate class attributes when__init_subclass__does the same thing in three lines. Metaclasses add complexity, restrict inheritance, and confuse other developers who encounter them. -
Magic attribute injection — a metaclass that silently adds methods, attributes, or modifies the class namespace in ways that are not visible in the source code. Users of the class see behavior that does not correspond to any code they can read, making debugging a forensic exercise.
-
Descriptor without set_name — defining a descriptor that requires manual name passing (
name = ValidatedField("name")) instead of implementing__set_name__to capture the attribute name automatically. This duplicates information and breaks when someone renames the attribute but forgets to update the string. -
Mutable class-level state in metaclasses — storing a shared registry or configuration dict as a metaclass class variable without considering that all classes sharing the metaclass mutate the same dict. This creates action-at-a-distance bugs where defining a class in one module affects behavior in another.
-
Multiple inheritance with competing metaclasses — designing a class hierarchy where two base classes use different, unrelated metaclasses. Python raises
TypeErrorbecause it cannot determine which metaclass to use. This is a fundamental design problem that requires restructuring, not a trick to work around.
Install this skill directly: skilldb add python-patterns-skills
Related Skills
Async Patterns
Asyncio patterns for concurrent I/O-bound programming in Python
Context Managers
Context manager patterns using with statements for reliable resource management in Python
Dataclasses
Dataclass and Pydantic model patterns for structured data in Python
Decorators
Decorator patterns for wrapping, extending, and composing Python functions and classes
Dependency Injection
Dependency injection patterns for loosely coupled, testable Python applications
Generators
Generator and itertools patterns for memory-efficient data processing in Python