9 min read
ā¢Question 28 of 41hardMetaclasses in Python
Understanding the class of a class.
What You'll Learn
- What metaclasses are and how they work
- The relationship between classes and type
- Creating custom metaclasses
- Practical use cases: singletons, registries, validation
- When to use metaclasses vs alternatives
Understanding Metaclasses
In Python, everything is an object, including classes. A metaclass is the "class of a class" ā it defines how classes themselves behave.
code.pyPython
# The hierarchy:
# instance ā class ā metaclass
class MyClass:
pass
obj = MyClass()
print(type(obj)) # <class '__main__.MyClass'>
print(type(MyClass)) # <class 'type'>
print(type(type)) # <class 'type'>
# 'type' is:
# 1. The default metaclass for all classes
# 2. Its own metaclass (type's type is type)
# 3. The way to create classes dynamicallyCreating Classes with type()
code.pyPython
# Normal class definition
class Dog:
species = "Canis familiaris"
def bark(self):
return "Woof!"
# Equivalent using type()
def bark(self):
return "Woof!"
Dog = type(
'Dog', # Class name
(), # Base classes (tuple)
{ # Namespace (dict)
'species': 'Canis familiaris',
'bark': bark
}
)
# Both create identical classes
d = Dog()
print(d.bark()) # "Woof!"Creating a Custom Metaclass
code.pyPython
class Meta(type):
def __new__(mcs, name, bases, namespace):
"""Called when a CLASS is created (not instance)."""
print(f"Creating class: {name}")
print(f"Bases: {bases}")
print(f"Namespace keys: {list(namespace.keys())}")
return super().__new__(mcs, name, bases, namespace)
def __init__(cls, name, bases, namespace):
"""Called after class is created."""
print(f"Initializing class: {name}")
super().__init__(name, bases, namespace)
def __call__(cls, *args, **kwargs):
"""Called when class is instantiated."""
print(f"Creating instance of {cls.__name__}")
return super().__call__(*args, **kwargs)
class MyClass(metaclass=Meta):
x = 10
def method(self):
pass
# Output when class is defined:
# Creating class: MyClass
# Bases: ()
# Namespace keys: ['__module__', '__qualname__', 'x', 'method']
# Initializing class: MyClass
obj = MyClass()
# Output: Creating instance of MyClassPractical Use Cases
Singleton Pattern
code.pyPython
class SingletonMeta(type):
_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):
print("Initializing database connection")
self.connected = True
db1 = Database() # Initializing database connection
db2 = Database() # (nothing printed - cached)
print(db1 is db2) # TruePlugin Registry
code.pyPython
class PluginRegistry(type):
plugins = {}
def __new__(mcs, name, bases, namespace):
cls = super().__new__(mcs, name, bases, namespace)
if bases: # Don't register the base class
mcs.plugins[name] = cls
return cls
class Plugin(metaclass=PluginRegistry):
"""Base class for all plugins."""
pass
class ImagePlugin(Plugin):
def process(self): return "Processing image"
class VideoPlugin(Plugin):
def process(self): return "Processing video"
class AudioPlugin(Plugin):
def process(self): return "Processing audio"
# All plugins auto-registered
print(PluginRegistry.plugins)
# {'ImagePlugin': <class 'ImagePlugin'>, 'VideoPlugin': ...}
# Use dynamically
def process_file(plugin_name):
plugin_cls = PluginRegistry.plugins[plugin_name]
return plugin_cls().process()Attribute Validation
code.pyPython
class ValidatedMeta(type):
def __new__(mcs, name, bases, namespace):
# Ensure all methods have docstrings
for key, value in namespace.items():
if callable(value) and not key.startswith('_'):
if not value.__doc__:
raise TypeError(f"Method {key} must have docstring")
return super().__new__(mcs, name, bases, namespace)
class APIHandler(metaclass=ValidatedMeta):
def get(self):
"""Handle GET requests."""
pass
def post(self):
"""Handle POST requests."""
pass
# def delete(self): # Would raise TypeError - no docstring!
# passAbstract Base Class Pattern
code.pyPython
class InterfaceMeta(type):
def __new__(mcs, name, bases, namespace):
cls = super().__new__(mcs, name, bases, namespace)
# Check if abstract methods are implemented
if bases: # Not the base interface itself
for base in bases:
if hasattr(base, '_abstract_methods'):
for method in base._abstract_methods:
if method not in namespace:
raise TypeError(
f"Class {name} must implement {method}"
)
return cls
class Serializable(metaclass=InterfaceMeta):
_abstract_methods = ['serialize', 'deserialize']
class JSONData(Serializable):
def serialize(self): return "{}"
def deserialize(self, data): pass
# class BrokenData(Serializable): # TypeError!
# def serialize(self): pass
# # Missing deserializeMetaclasses vs Alternatives
| Feature | Metaclass | Class Decorator | init_subclass |
|---|---|---|---|
| Control | Full class creation | Post-creation modification | Subclass hook |
| Complexity | High | Medium | Low |
| Inheritance | Automatic | Must reapply | Automatic |
| Use Case | Deep customization | Simple modifications | Subclass validation |
code.pyPython
# __init_subclass__ - simpler alternative (Python 3.6+)
class Plugin:
plugins = {}
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
Plugin.plugins[cls.__name__] = cls
class ImagePlugin(Plugin):
pass
print(Plugin.plugins) # {'ImagePlugin': <class 'ImagePlugin'>}Interview Tip
When asked about metaclasses:
- "type" is the default metaclass; classes are instances of type
- new creates the class, call creates instances
- Use cases: singletons, registries, validation, ORMs
- Consider simpler alternatives: decorators, init_subclass
- "If you're not sure if you need a metaclass, you don't"