Metaclasses & Descriptors
Deep dive into Python's object model and metaprogramming
Metaclasses and descriptors form the foundation of Python's runtime behavior and are heavily used by frameworks such as Django, SQLAlchemy, Pydantic, and dataclasses. The goal is not memorization, but building a correct mental model of how Python creates classes and resolves attribute access.
Python's Object Model
In Python, everything is an object, including classes themselves. This creates a hierarchy: instances → classes → metaclasses.
# Everything is an object x = 10 print(x.__class__) # <class 'int'> print(int.__class__) # <class 'type'> print(type.__class__) # <class 'type'> # type is its own metaclass! print(type(type)) # <class 'type'>
type)Interactive Demonstrations
Temperature Descriptor
Descriptors control attribute access. This example shows a descriptor that converts between Celsius and Kelvin, with validation.
class Celsius:
"""Descriptor that stores temperature in Kelvin"""
def __set_name__(self, owner, name):
self.private_name = f'_{name}'
def __get__(self, obj, objtype=None):
if obj is None:
return self
kelvin = getattr(obj, self.private_name)
return kelvin - 273.15 # Convert to Celsius
def __set__(self, obj, value):
if value < -273.15:
raise ValueError("Temperature below absolute zero!")
kelvin = value + 273.15
setattr(obj, self.private_name, kelvin)
class Temperature:
celsius = Celsius() # Descriptor instance
# Example Usage:
temp = Temperature()
# Set the temperature in Celsius (internally stored as Kelvin)
temp.celsius = 25.0
print(f"Temperature (C): {temp.celsius}") # Output: Temperature (C): 25.0
# Attempt to set a value below absolute zero
try:
temp.celsius = -300.0
except ValueError as e:
print(e) # Output: Temperature below absolute zero!
# The original value remains unchanged after the error
print(f"Current Temperature (C): {temp.celsius}") # Output: Current Temperature (C): 25.0
# Accessing the underlying private attribute (in Kelvin)
# print(temp._celsius) # This would output 298.15 (25 + 273.15)The Descriptor Protocol
A descriptor is any object that implements one or more of these methods:
__get__
__get__(self, instance, owner)Called when the attribute is accessed
__set__
__set__(self, instance, value)Called when the attribute is assigned
__delete__
__delete__(self, instance)Called when the attribute is deleted
- Data descriptors define both
__get__and__set__(or__delete__). They take priority over instance attributes. - Non-data descriptors only define
__get__. Instance attributes can override them (this is how methods work!).
__set_name__: Descriptor Initialization
class Field:
def __set_name__(self, owner, name):
# Called automatically when descriptor is assigned to class
self.private_name = f"_{name}"
def __get__(self, obj, objtype=None):
if obj is None:
return self
return getattr(obj, self.private_name)
def __set__(self, obj, value):
setattr(obj, self.private_name, value)
class Example:
# __set_name__ is called with owner=Example, name='field'
field = Field()__set_name__ eliminates boilerplate and enables reusable descriptors without manual name tracking.__dict__ (using a private name like _age). Older tutorials might suggest WeakKeyDictionary to avoid naming conflicts, but __set_name__ makes that largely obsolete.Metaclasses: Classes for Classes
A metaclass is a class that creates other classes. If classes define behavior for instances, metaclasses define behavior for classes themselves.
class Meta(type):
def __new__(cls, name, bases, namespace):
# Called when a class using this metaclass is created
namespace['created_by_meta'] = True
namespace['class_name'] = name
return super().__new__(cls, name, bases, namespace)
def __init__(cls, name, bases, namespace):
# Called after the class is created
super().__init__(name, bases, namespace)
print(f"Class {name} initialized")
class Example(metaclass=Meta):
pass
print(Example.created_by_meta) # True
print(Example.class_name) # 'Example'__init_subclass__ (Python 3.6+) as a simpler alternative for many use cases.Modern Alternative: __init_subclass__
Instead of a metaclass for registration, use __init_subclass__. It's a standard method called when a class is subclassed.
The Old Way (Metaclass)
class RegistryMeta(type):
def __init__(cls, name, bases, ns):
super().__init__(name, bases, ns)
if name != 'Base':
register(cls)
class Base(metaclass=RegistryMeta):
passThe Modern Way (__init_subclass__)
class Base:
@classmethod
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
register(cls)
# No metaclass conflict!
class User(Base):
passBonus: Reusable Factories with core-mixins
To avoid rewriting registration logic in every project, consider using thecore-mixins library.
The core_mixins.interfaces.IFactory interface provides a battle-tested implementation for:
- Automatic registration of concrete implementations
- Runtime discovery of available plugins
- Instantiation by string reference
- Separated registries for different factory types
Real-World Usage
Descriptors Used In:
- @property - Python's built-in decorator
- Methods - Functions bound to instances
- Django Models - Field definitions
- SQLAlchemy - Column declarations
Metaclasses Used In:
- ABCMeta - Abstract base classes
- Enum - Enumeration types
- Django ORM - Model registration
- Singletons - Class-level control
__init_subclass__ can solve your problem, prefer them over metaclasses.Key Takeaways
- Descriptors control attribute access on instances from the class level
- Metaclasses control class creation and can modify classes as they're defined
- Most Python "magic" builds on these primitives (@property, methods, ORMs, etc.)
- Power comes with complexity, use intentionally and prefer simpler alternatives when possible
- __set_name__ and __init_subclass__ are modern, simpler alternatives to consider first
What's Next?
You've learned metaprogramming! Now let's explore memory management and optimization to write high-performance Python code.
- Garbage Collection - Understand Python's memory management
- Memory Profiling - Identify and fix memory leaks
- Optimization Techniques - Use __slots__, weak references, and more