Metaclasses: type, new, init_subclass, Descriptors
Understanding Python's object model and metaprogramming
Interview Question
"Explain Python metaclasses. What is type and how does it relate to classes? How do __new__ and __init__ differ? When would you use metaclasses vs class decorators vs __init_subclass__?"
Difficulty: Hard | Frequently asked at Google, Meta, Microsoft
Theoretical Foundation
Everything is an Object
In Python, everything is an object, including classes themselves.
# Classes are objects
class MyClass:
pass
print(f"MyClass type: {type(MyClass)}")
print(f"MyClass id: {id(MyClass)}")
print(f"MyClass dict: {MyClass.__dict__}")
# type is the metaclass of all classes
print(f"\ntype(MyClass): {type(MyClass)}")
print(f"type(type): {type(type)}")
print(f"type is its own type: {type is type(MyClass)}")
Output:
MyClass type: <class 'type'>
MyClass id: 140234866577152
MyClass dict: {'__module__': '__main__', '__dict__': <attribute '__dict__' of 'MyClass' objects>, '__weakref__': <...>, '__doc__': None}
type(MyClass): <class 'type'>
type(type): <class 'type'>
type is its own type: True
ℹ️
Key Concept: type is the metaclass for all classes. When you define a class, type is used to create it.
Creating Classes Dynamically
# Using type() to create classes
def __init__(self, name, age):
self.name = name
self.age = age
def greet(self):
return f"Hello, {self.name}!"
# type(name, bases, dict)
Person = type('Person', (), {'__init__': __init__, 'greet': greet})
# Usage
person = Person("Alice", 30)
print(person.greet())
print(f"Person type: {type(Person)}")
# With inheritance
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
raise NotImplementedError
def dog_speak(self):
return f"{self.name} says Woof!"
# Create Dog class dynamically
Dog = type('Dog', (Animal,), {'speak': dog_speak})
dog = Dog("Buddy")
print(dog.speak())
print(f"Dog bases: {Dog.__bases__}")
Output:
Hello, Alice!
Person type: <class 'type'>
Buddy says Woof!
Dog bases: (<class '__main__.Animal'>,)
Metaclasses
Basic Metaclass
class MyMeta(type):
"""Custom metaclass."""
def __new__(cls, name, bases, dict):
# Called when class is created
print(f"Creating class: {name}")
print(f"Bases: {bases}")
# Add custom attributes
dict['class_id'] = name.lower()
dict['created_by'] = 'MyMeta'
return super().__new__(cls, name, bases, dict)
def __init__(cls, name, bases, dict):
# Called after class is created
print(f"Initializing class: {name}")
super().__init__(name, bases, dict)
def __call__(cls, *args, **kwargs):
# Called when class is instantiated
print(f"Instantiating class: {cls.__name__}")
instance = super().__call__(*args, **kwargs)
return instance
# Using metaclass
class MyClass(metaclass=MyMeta):
def __init__(self, value):
self.value = value
# Creating instance
obj = MyClass(42)
print(f"obj.value: {obj.value}")
print(f"MyClass.class_id: {MyClass.class_id}")
print(f"MyClass.created_by: {MyClass.created_by}")
Output:
Creating class: MyClass
Bases: ()
Initializing class: MyClass
Instantiating class: MyClass
obj.value: 42
MyClass.class_id: myclass
MyClass.created_by: MyMeta
new vs init
class MyClass:
def __new__(cls, *args, **kwargs):
print("__new__ called")
# Create the instance
instance = super().__new__(cls)
return instance
def __init__(self, value):
print("__init__ called")
self.value = value
# Usage
print("Creating instance:")
obj = MyClass(42)
print(f"obj.value: {obj.value}")
print("\nCreating another instance:")
obj2 = MyClass(100)
print(f"obj2.value: {obj2.value}")
Output:
Creating instance:
__new__ called
__init__ called
obj.value: 42
Creating another instance:
__new__ called
__init__ called
obj2.value: 100
⚠️
Key Difference: __new__ creates the instance, __init__ initializes it. __new__ is called first.
Singleton Pattern with Metaclass
class SingletonMeta(type):
"""Metaclass for Singleton pattern."""
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super().__call__(*args, **kwargs)
return cls._instances[cls]
class Database(metaclass=SingletonMeta):
def __init__(self):
self.connection = "Connected"
def query(self, sql):
return f"Executing: {sql}"
# Usage
db1 = Database()
db2 = Database()
print(f"db1 is db2: {db1 is db2}")
print(f"db1.connection: {db1.connection}")
print(f"db2.query('SELECT *'): {db2.query('SELECT *')}")
Output:
db1 is db2: True
db1.connection: Connected
db2.query('SELECT *'): Executing: SELECT *
init_subclass
Modern Alternative to Metaclasses
class BasePlugin:
"""Base class for plugins using __init_subclass__."""
_plugins = {}
def __init_subclass__(cls, plugin_name=None, **kwargs):
super().__init_subclass__(**kwargs)
# Register plugin
name = plugin_name or cls.__name__
BasePlugin._plugins[name] = cls
@classmethod
def get_plugin(cls, name):
return cls._plugins.get(name)
class JSONPlugin(BasePlugin, plugin_name="json"):
def process(self, data):
return f"JSON: {data}"
class XMLPlugin(BasePlugin, plugin_name="xml"):
def process(self, data):
return f"XML: {data}"
# Usage
print(f"Available plugins: {list(BasePlugin._plugins.keys())}")
json_plugin = JSONPlugin()
xml_plugin = XMLPlugin()
print(json_plugin.process("data"))
print(xml_plugin.process("data"))
# Get plugin by name
PluginClass = BasePlugin.get_plugin("json")
plugin = PluginClass()
print(plugin.process("test"))
Output:
Available plugins: ['json', 'xml']
JSON: data
XML: data
JSON: test
Validation with init_subclass
class Validated:
"""Base class that validates subclass attributes."""
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
# Check for required attributes
if not hasattr(cls, 'name'):
raise TypeError(f"Class {cls.__name__} must define 'name'")
if not isinstance(cls.name, str):
raise TypeError(f"{cls.__name__}.name must be a string")
class User(Validated):
name = "user" # Required
email = "user@example.com"
class Admin(Validated):
name = "admin" # Required
permissions = ["read", "write"]
# This would raise TypeError
# class BadClass(Validated):
# pass # Missing 'name'
print(f"User.name: {User.name}")
print(f"Admin.name: {Admin.name}")
💡
Interview Tip: __init_subclass__ is often simpler and more Pythonic than metaclasses for many use cases.
Descriptor Protocol
Understanding Descriptors
Descriptors are objects that define __get__, __set__, or __delete__.
class Property:
"""Simple property descriptor."""
def __init__(self, fget=None, fset=None, fdel=None):
self.fget = fget
self.fset = fset
self.fdel = fdel
def __get__(self, obj, objtype=None):
if obj is None:
return self
if self.fget is None:
raise AttributeError("unreadable attribute")
return self.fget(obj)
def __set__(self, obj, value):
if self.fset is None:
raise AttributeError("can't set attribute")
self.fset(obj, value)
def __delete__(self, obj):
if self.fdel is None:
raise AttributeError("can't delete attribute")
self.fdel(obj)
def getter(self, fget):
return type(self)(fget, self.fset, self.fdel)
def setter(self, fset):
return type(self)(self.fget, fset, self.fdel)
def deleter(self, fdel):
return type(self)(self.fget, self.fset, fdel)
# Usage
class Temperature:
def __init__(self):
self._celsius = 0
@Property
def celsius(self):
return self._celsius
@celsius.setter
def celsius(self, value):
if value < -273.15:
raise ValueError("Temperature below absolute zero")
self._celsius = value
@Property
def fahrenheit(self):
return self._celsius * 9/5 + 32
@fahrenheit.setter
def fahrenheit(self, value):
self._celsius = (value - 32) * 5/9
# Usage
temp = Temperature()
temp.celsius = 100
print(f"Celsius: {temp.celsius}")
print(f"Fahrenheit: {temp.fahrenheit}")
temp.fahrenheit = 32
print(f"New Celsius: {temp.celsius}")
Output:
Celsius: 100
Fahrenheit: 212.0
New Celsius: 0.0
Class and Static Methods as Descriptors
class ClassMethod:
"""Class method descriptor."""
def __init__(self, func):
self.func = func
def __get__(self, obj, objtype=None):
if objtype is None:
objtype = type(obj)
def method(*args, **kwargs):
return self.func(objtype, *args, **kwargs)
return method
class StaticMethod:
"""Static method descriptor."""
def __init__(self, func):
self.func = func
def __get__(self, obj, objtype=None):
return self.func
class MyClass:
@ClassMethod
def create(cls, value):
return cls(value)
@StaticMethod
def utility(x, y):
return x + y
# Usage
obj = MyClass.create(42)
print(f"Created: {obj}")
print(f"Utility: {MyClass.utility(1, 2)}")
Metaclass Applications
Validation Metaclass
class ValidatedMeta(type):
"""Metaclass that validates class definitions."""
def __new__(cls, name, bases, dict):
# Skip validation for base class
if not bases:
return super().__new__(cls, name, bases, dict)
# Validate required methods
required_methods = ['process', 'validate']
for method in required_methods:
if method not in dict:
raise TypeError(f"Class {name} must implement '{method}'")
# Validate type hints
annotations = dict.get('__annotations__', {})
for attr, expected_type in annotations.items():
if attr in dict:
value = dict[attr]
if not isinstance(value, expected_type):
raise TypeError(
f"{name}.{attr} must be {expected_type}, got {type(value)}"
)
return super().__new__(cls, name, bases, dict)
class BaseHandler(metaclass=ValidatedMeta):
"""Base handler with validation."""
pass
# This works
class ValidHandler(BaseHandler):
name: str = "valid"
def process(self, data):
return data
def validate(self, data):
return True
# This would raise TypeError
# class InvalidHandler(BaseHandler):
# name: str = "valid"
# # Missing process and validate methods
print(f"ValidHandler.name: {ValidHandler.name}")
Registry Metaclass
class RegistryMeta(type):
"""Metaclass that maintains a registry of classes."""
_registry = {}
def __new__(cls, name, bases, dict):
new_class = super().__new__(cls, name, bases, dict)
# Don't register the base class
if bases:
cls._registry[name] = new_class
return new_class
@classmethod
def get_registry(cls):
return cls._registry.copy()
@classmethod
def get_class(cls, name):
return cls._registry.get(name)
class Serializer(metaclass=RegistryMeta):
"""Base serializer class."""
pass
class JSONSerializer(Serializer):
def serialize(self, data):
return f"JSON: {data}"
class XMLSerializer(Serializer):
def serialize(self, data):
return f"XML: {data}"
# Usage
print(f"Registry: {list(RegistryMeta.get_registry().keys())}")
json_serializer = JSONSerializer()
print(json_serializer.serialize("data"))
# Get class from registry
SerializerClass = RegistryMeta.get_class("XMLSerializer")
serializer = SerializerClass()
print(serializer.serialize("data"))
ℹ️
Pattern: Metaclass registries are used in frameworks like Django and SQLAlchemy for automatic class registration.
Metaclasses vs Alternatives
Comparison
# 1. Metaclass approach
class MetaApproach(type):
def __new__(cls, name, bases, dict):
dict['created_by'] = 'Meta'
return super().__new__(cls, name, bases, dict)
class Class1(metaclass=MetaApproach):
pass
# 2. Class decorator approach
def class_decorator(cls):
cls.created_by = 'Decorator'
return cls
@class_decorator
class Class2:
pass
# 3. __init_subclass__ approach
class Base:
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
cls.created_by = 'InitSubclass'
class Class3(Base):
pass
# All achieve similar results
print(f"Class1.created_by: {Class1.created_by}")
print(f"Class2.created_by: {Class2.created_by}")
print(f"Class3.created_by: {Class3.created_by}")
When to Use What?
| Technique | Use Case | Complexity |
|---|---|---|
| Metaclass | Complex class modification, framework internals | High |
| Class Decorator | Simple class modification | Low |
| init_subclass | Subclass registration, validation | Medium |
| Descriptor | Attribute access control | Medium |
💡
Interview Tip: Start with simpler alternatives (decorators, __init_subclass__). Only use metaclasses when necessary.
Practical Examples
ORM-Style Field Definition
class Field:
"""Base field descriptor."""
def __init__(self, field_type, required=True, default=None):
self.field_type = field_type
self.required = required
self.default = default
def __set_name__(self, owner, name):
self.name = name
self.private_name = f'_{name}'
def __get__(self, obj, objtype=None):
if obj is None:
return self
return getattr(obj, self.private_name, self.default)
def __set__(self, obj, value):
if value is None and self.required:
raise ValueError(f"{self.name} is required")
if value is not None and not isinstance(value, self.field_type):
raise TypeError(f"{self.name} must be {self.field_type}")
setattr(obj, self.private_name, value)
class ModelMeta(type):
"""Metaclass for model classes."""
def __new__(cls, name, bases, dict):
fields = {}
# Collect fields from base classes
for base in bases:
if hasattr(base, '_fields'):
fields.update(base._fields)
# Collect fields from current class
for key, value in dict.items():
if isinstance(value, Field):
fields[key] = value
dict['_fields'] = fields
return super().__new__(cls, name, bases, dict)
class Model(metaclass=ModelMeta):
"""Base model class."""
def __init__(self, **kwargs):
for field_name, field in self._fields.items():
value = kwargs.get(field_name, field.default)
setattr(self, field_name, value)
def to_dict(self):
return {name: getattr(self, name) for name in self._fields}
class User(Model):
name = Field(str, required=True)
email = Field(str, required=True)
age = Field(int, required=False, default=0)
# Usage
user = User(name="Alice", email="alice@example.com", age=30)
print(f"User: {user.to_dict()}")
# Validation works
try:
bad_user = User(name=123) # Wrong type
except TypeError as e:
print(f"Error: {e}")
Output:
User: {'name': 'Alice', 'email': 'alice@example.com', 'age': 30}
Error: name must be <class 'str'>
API Endpoint Registration
class APIMeta(type):
"""Metaclass for API endpoint registration."""
_endpoints = {}
def __new__(cls, name, bases, dict):
new_class = super().__new__(cls, name, bases, dict)
if bases: # Not base class
# Register methods decorated with @endpoint
for method_name, method in dict.items():
if hasattr(method, '_endpoint'):
path = method._endpoint
cls._endpoints[path] = {
'class': new_class,
'method': method_name,
'methods': method._methods
}
return new_class
@classmethod
def get_endpoints(cls):
return cls._endpoints.copy()
def endpoint(path, methods=None):
"""Decorator to register API endpoint."""
if methods is None:
methods = ['GET']
def decorator(func):
func._endpoint = path
func._methods = methods
return func
return decorator
class UserAPI(metaclass=APIMeta):
@endpoint('/users', methods=['GET'])
def list_users(self):
return "List users"
@endpoint('/users', methods=['POST'])
def create_user(self):
return "Create user"
@endpoint('/users/{id}', methods=['GET'])
def get_user(self, user_id):
return f"Get user {user_id}"
# Usage
print("Registered endpoints:")
for path, info in APIMeta.get_endpoints().items():
print(f" {path}: {info['method']} ({info['methods']})")
Output:
Registered endpoints:
/users: list_users (['GET'])
/users: create_user (['POST'])
/users/{id}: get_user (['GET'])
ℹ️
Framework Pattern: This pattern is used in FastAPI, Django, and other web frameworks for automatic route registration.
Complexity Analysis
Performance Impact
| Operation | Without Metaclass | With Metaclass | Overhead |
|---|---|---|---|
| Class creation | O(1) | O(n) | One-time |
| Instance creation | O(1) | O(1) | Negligible |
| Attribute access | O(1) | O(1) | Negligible |
Memory Usage
import sys
class RegularClass:
def __init__(self):
self.value = 42
class MetaClass(type):
def __new__(cls, name, bases, dict):
dict['meta_attr'] = 'meta'
return super().__new__(cls, name, bases, dict)
class MetaclassedClass(metaclass=MetaClass):
def __init__(self):
self.value = 42
# Compare memory
regular = RegularClass()
metaclassed = MetaclassedClass()
print(f"RegularClass size: {sys.getsizeof(regular)} bytes")
print(f"MetaclassedClass size: {sys.getsizeof(metaclassed)} bytes")
Interview Tips
Common Follow-up Questions
-
"What's the difference between
__new__and__init__?"__new__: Creates the instance (static method)__init__: Initializes the instance__new__is called first
-
"When would you use a metaclass?"
- Class registration
- Validation
- Modification of class behavior
- Framework internals
-
"What are the alternatives to metaclasses?"
- Class decorators
__init_subclass__- Descriptors
- Simple inheritance
Code Review Tips
# BAD: Unnecessary metaclass
class OverkillMeta(type):
pass
class MyClass(metaclass=OverkillMeta):
pass
# GOOD: Simple class decorator
def simple_decorator(cls):
cls.created_by = 'Decorator'
return cls
@simple_decorator
class MyClass:
pass
# BAD: Metaclass for simple registration
class RegistryMeta(type):
_registry = {}
def __new__(cls, name, bases, dict):
cls._registry[name] = super().__new__(cls, name, bases, dict)
return cls._registry[name]
# GOOD: __init_subclass__ for registration
class Registry:
_registry = {}
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
Registry._registry[cls.__name__] = cls
⚠️
Common Mistake: Using metaclasses when simpler alternatives exist. Always prefer decorators or __init_subclass__ first.
Summary
| Concept | Purpose | When to Use |
|---|---|---|
| type | Create/inspect classes | Dynamic class creation |
| Metaclass | Modify class creation | Complex class modification |
| new | Create instances | Singleton, immutable types |
| init_subclass | Subclass hook | Registration, validation |
| Descriptor | Attribute access | Properties, computed attrs |
Best Practices
- Prefer simpler alternatives (decorators,
__init_subclass__) - Use metaclasses sparingly for complex cases
- Document metaclass usage clearly
- Test metaclass behavior thoroughly
- Consider readability over cleverness
ℹ️
Key Takeaway: Metaclasses are powerful but complex. Use them only when simpler alternatives won't work.
Practice Problems
- Singleton Metaclass: Implement a thread-safe singleton metaclass
- Validation Metaclass: Create a metaclass that validates class attributes
- Registry Pattern: Build a plugin registry using metaclasses
- ORM Fields: Implement database field descriptors
- API Router: Create an automatic API route registration system
Further Reading
- PEP 3119: Abstract Base Classes
- PEP 3135: New super()
- Python Docs:
type,object.__init_subclass__ - Books: "Python Metaprogramming" by David Mertz
Remember: Metaclasses are the "class of a class." They control how classes are created and behave. Use them wisely.