type() — The Class of Classes
In Python, everything is an object — including classes themselves. When you write class Dog: ..., Python creates a class object. But what creates that class object? The answer is type. Just as Dog creates dog instances, type creates class instances. This makes type a metaclass — a class whose instances are themselves classes.
# Every object has a type
print(type(42)) #
print(type("hello")) #
print(type([1, 2, 3])) #
# Classes are objects too — their type is 'type'
class Dog:
pass
print(type(Dog)) #
print(type(int)) #
print(type(type)) # — type is its own metaclass!
<class 'int'> <class 'str'> <class 'list'> <class 'type'> <class 'type'> <class 'type'>
Creating Classes Dynamically with type()
Since type creates classes, you can create them at runtime without the class keyword. This is rarely needed in application code, but understanding it is key to understanding metaclasses:
# Normal class definition
class Dog:
species = "Canis familiaris"
def bark(self):
return "Woof!"
# Equivalent using type() directly
Dog = type('Dog', (), {
'species': 'Canis familiaris',
'bark': lambda self: 'Woof!',
})
rex = Dog()
print(rex.bark())
print(rex.species)
Woof! Canis familiaris
type(name, bases, namespace) takes three arguments: the class name, a tuple of base classes, and a dictionary of attributes/methods. This is exactly what Python does internally when it encounters a class statement.
Custom Metaclasses
A custom metaclass lets you intercept and modify class creation. You subclass type and override __new__ (which creates the class) or __init__ (which initializes it):
class ValidatedMeta(type):
"""Metaclass that ensures all public methods have docstrings."""
def __new__(mcs, name, bases, namespace):
for key, value in namespace.items():
if callable(value) and not key.startswith('_'):
if not getattr(value, '__doc__', None):
raise TypeError(
f"{name}.{key}() is missing a docstring"
)
return super().__new__(mcs, name, bases, namespace)
class APIEndpoint(metaclass=ValidatedMeta):
def get(self):
"""Handle GET requests."""
return "OK"
def post(self):
"""Handle POST requests."""
return "Created"
print("APIEndpoint created successfully!")
# This would FAIL:
# class BadEndpoint(metaclass=ValidatedMeta):
# def get(self): # No docstring!
# return "OK"
# TypeError: BadEndpoint.get() is missing a docstring
APIEndpoint created successfully!
When to Use Metaclasses
Metaclasses are powerful but rarely needed. Use them for: enforcing coding standards across many classes, automatic registration (plugin systems), ORM magic (SQLAlchemy uses them), and API frameworks. If you're not sure you need a metaclass, you probably don't.
__init_subclass__ — The Simpler Alternative
Python 3.6 added __init_subclass__ as a simpler way to customize class creation without writing a full metaclass. It's called automatically whenever a class is subclassed:
class Plugin:
"""Base class that auto-registers subclasses."""
_registry = {}
def __init_subclass__(cls, name=None, **kwargs):
super().__init_subclass__(**kwargs)
plugin_name = name or cls.__name__.lower()
Plugin._registry[plugin_name] = cls
print(f" Registered plugin: {plugin_name}")
class JSONPlugin(Plugin, name="json"):
def process(self, data):
import json
return json.dumps(data)
class XMLPlugin(Plugin, name="xml"):
def process(self, data):
return f"{data}"
class CSVPlugin(Plugin): # No name — uses class name
def process(self, data):
return str(data)
print(f"\nRegistry: {list(Plugin._registry.keys())}")
# Create plugin by name
plugin = Plugin._registry["json"]()
print(plugin.process({"key": "value"}))
Registered plugin: json
Registered plugin: xml
Registered plugin: csvplugin
Registry: ['json', 'xml', 'csvplugin']
{"key": "value"}__init_subclass__ instead of metaclasses when possible. It covers 90% of use cases (registration, validation, modification) with much simpler code.The Descriptor Protocol
Descriptors are the mechanism behind @property, @classmethod, @staticmethod, and how attribute access works in Python. A descriptor is any object that implements __get__, __set__, or __delete__:
class Validated:
"""A descriptor that validates values on assignment."""
def __init__(self, min_value=None, max_value=None):
self.min_value = min_value
self.max_value = max_value
def __set_name__(self, owner, name):
"""Called when the descriptor is assigned to a class attribute."""
self.name = name
def __get__(self, obj, objtype=None):
if obj is None:
return self # Accessed from class, not instance
return obj.__dict__.get(self.name)
def __set__(self, obj, value):
if self.min_value is not None and value < self.min_value:
raise ValueError(f"{self.name} must be >= {self.min_value}, got {value}")
if self.max_value is not None and value > self.max_value:
raise ValueError(f"{self.name} must be <= {self.max_value}, got {value}")
obj.__dict__[self.name] = value
class Product:
price = Validated(min_value=0)
quantity = Validated(min_value=0, max_value=1000)
p = Product()
p.price = 29.99
p.quantity = 5
print(f"Product: ${p.price}, qty: {p.quantity}")
try:
p.price = -10 # ValueError!
except ValueError as e:
print(f"Error: {e}")
try:
p.quantity = 5000 # ValueError!
except ValueError as e:
print(f"Error: {e}")
Product: $29.99, qty: 5 Error: price must be >= 0, got -10 Error: quantity must be <= 1000, got 5000
__set_name__ (Python 3.6+) is called automatically when the descriptor is assigned to a class attribute, passing the attribute name. This eliminates the need to repeat the name manually.
@property is a Descriptor
The @property decorator you learned in the OOP tutorial is actually implemented as a descriptor:
# These two are equivalent:
# Using @property
class Circle:
def __init__(self, radius):
self._radius = radius
@property
def radius(self):
return self._radius
# Using property() descriptor directly
class Circle:
def __init__(self, radius):
self._radius = radius
def _get_radius(self):
return self._radius
radius = property(_get_radius) # Descriptor!
🔍 Deep Dive: Data vs Non-Data Descriptors
A data descriptor implements both __get__ and __set__ (like property). A non-data descriptor implements only __get__ (like regular methods and classmethod). The difference matters for attribute lookup order: data descriptors take priority over instance __dict__, while non-data descriptors don't. This is why you can override methods on instances but not properties.
Practical Uses
| Pattern | Mechanism | Example |
|---|---|---|
| Plugin registration | __init_subclass__ | Auto-register handlers by subclassing |
| Attribute validation | Descriptors | Type/range checking on assignment |
| ORM fields | Descriptors + metaclass | SQLAlchemy Column() |
| API serialization | Metaclass | Pydantic BaseModel |
| Abstract methods | Metaclass (ABCMeta) | @abstractmethod |
| Singleton | Metaclass with __call__ | One instance per class |
⚠️ Common Mistake: Overusing Metaclasses
Wrong: Using a metaclass when a simpler solution exists.
Why: Metaclasses add complexity, are hard to debug, and confuse other developers. Tim Peters (author of The Zen of Python) said: "Metaclasses are deeper magic than 99% of users should ever worry about."
Instead: Try these alternatives first: __init_subclass__, class decorators, descriptors, or regular inheritance. Only reach for metaclasses when nothing else works.