How to Add Attributes in Python Metaclasses
Metaclasses are one of Python's most powerful and advanced features. They let you customize how classes are created, enabling you to automatically add attributes, validate class definitions, register classes, or enforce coding standards at the moment a class comes into existence.
This guide explains what metaclasses are, how they work, and demonstrates several ways to add attributes to classes through metaclasses.
Prerequisites
Before diving into metaclasses, you should be comfortable with:
- Python classes and objects (OOP fundamentals)
- The
type()function - Class inheritance
- Magic methods like
__init__and__new__
Understanding Metaclasses
In Python, everything is an object including classes themselves. When you define a class, Python creates a class object using a metaclass. By default, that metaclass is type.
The relationship is:
type (metaclass) → creates → Class (object) → creates → Instance (object)
class Dog:
pass
d = Dog()
print(type(d)) # <class '__main__.Dog'> where d is an instance of Dog
print(type(Dog)) # <class 'type'> where Dog is an instance of type
print(type(type)) # <class 'type'> where type is an instance of itself
Output:
<class '__main__.Dog'>
<class 'type'>
<class 'type'>
A metaclass is simply a class whose instances are classes. By creating a custom metaclass, you can control how classes are constructed.
Creating a Basic Metaclass
A metaclass is defined by inheriting from type. Override the __new__ method to customize class creation:
class MyMeta(type):
def __new__(cls, name, bases, namespace):
print(f"Creating class: {name}")
return super().__new__(cls, name, bases, namespace)
class MyClass(metaclass=MyMeta):
pass
Output:
Creating class: MyClass
The __new__ Parameters
| Parameter | Description |
|---|---|
cls | The metaclass itself (MyMeta) |
name | The name of the class being created ('MyClass') |
bases | Tuple of base classes (parent classes) |
namespace | Dictionary of the class body (attributes and methods) |
Adding Attributes via __new__
The most common way to add attributes through a metaclass is inside the __new__ method. Attributes added here appear on every class that uses the metaclass:
class AutoAttributeMeta(type):
def __new__(cls, name, bases, namespace):
# Create the class object
obj = super().__new__(cls, name, bases, namespace)
# Add attributes to the class
obj.created_by = "AutoAttributeMeta"
obj.version = "1.0"
return obj
class Product(metaclass=AutoAttributeMeta):
pass
class Service(metaclass=AutoAttributeMeta):
pass
print(f"Product.created_by: {Product.created_by}")
print(f"Product.version: {Product.version}")
print(f"Service.created_by: {Service.created_by}")
Output:
Product.created_by: AutoAttributeMeta
Product.version: 1.0
Service.created_by: AutoAttributeMeta
Both Product and Service automatically have created_by and version attributes, even though they weren't defined in their class bodies.
Adding Attributes as Metaclass Class Variables
You can also define attributes directly on the metaclass itself. Classes using that metaclass can access them through the standard attribute lookup chain:
class ConfigMeta(type):
debug_mode = False
log_level = "INFO"
def __new__(cls, name, bases, namespace):
obj = super().__new__(cls, name, bases, namespace)
obj.framework = "MyFramework"
return obj
class AppConfig(metaclass=ConfigMeta):
pass
# Attributes added in __new__
print("Framework:", AppConfig.framework)
# Attributes defined on the metaclass (accessible via metaclass)
print("Debug mode:", ConfigMeta.debug_mode)
print("Log level:", ConfigMeta.log_level)
Output:
Framework: MyFramework
Debug mode: False
Log level: INFO
Attributes defined on the metaclass (like debug_mode) are properties of the metaclass, not the class itself. They're accessible through type(AppConfig).debug_mode or ConfigMeta.debug_mode, but also through AppConfig.debug_mode via Python's attribute lookup.
Attributes set on the class object in __new__ (like framework) become direct attributes of the class.
Adding Dynamic Attributes Based on Class Content
One of the most powerful use cases is inspecting the class body and adding attributes dynamically based on what the class defines:
class AutoCountMeta(type):
def __new__(cls, name, bases, namespace):
obj = super().__new__(cls, name, bases, namespace)
# Count methods defined in the class
methods = [key for key, val in namespace.items()
if callable(val) and not key.startswith('_')]
obj._method_count = len(methods)
obj._method_names = methods
return obj
class Calculator(metaclass=AutoCountMeta):
def add(self, a, b):
return a + b
def subtract(self, a, b):
return a - b
def multiply(self, a, b):
return a * b
print(f"Calculator has {Calculator._method_count} methods: {Calculator._method_names}")
Output:
Calculator has 3 methods: ['add', 'subtract', 'multiply']
The metaclass automatically counts and records the public methods defined in the class.
Adding Attributes After Class Creation
You can add attributes to a metaclass (or its classes) at any time, just like with regular classes:
class DemoMeta(type):
attr1 = 1
def __new__(cls, name, bases, namespace):
obj = super().__new__(cls, name, bases, namespace)
obj.attr2 = 2
return obj
class Demo(metaclass=DemoMeta):
pass
print(f"attr1: {Demo.attr1}, attr2: {Demo.attr2}")
# Add a new attribute to the metaclass after creation
DemoMeta.attr3 = 3
print(f"attr3: {Demo.attr3}")
Output:
attr1: 1, attr2: 2
attr3: 3
Practical Example: Auto-Registration Pattern
A common real-world use of metaclasses is automatically registering classes in a registry when they're created:
class PluginMeta(type):
registry = {}
def __new__(cls, name, bases, namespace):
obj = super().__new__(cls, name, bases, namespace)
# Don't register the base class itself
if bases:
cls.registry[name] = obj
obj.registered = True
obj.plugin_name = name.lower()
return obj
class Plugin(metaclass=PluginMeta):
"""Base class for all plugins."""
pass
class AudioPlugin(Plugin):
def process(self):
return "Processing audio"
class VideoPlugin(Plugin):
def process(self):
return "Processing video"
# All subclasses are automatically registered
print("Registered plugins:", list(PluginMeta.registry.keys()))
print(f"AudioPlugin.plugin_name: {AudioPlugin.plugin_name}")
print(f"VideoPlugin.registered: {VideoPlugin.registered}")
# Instantiate a plugin by name
plugin = PluginMeta.registry['AudioPlugin']()
print(plugin.process())
Output:
Registered plugins: ['AudioPlugin', 'VideoPlugin']
AudioPlugin.plugin_name: audioplugin
VideoPlugin.registered: True
Processing audio
Practical Example: Enforcing Required Attributes
Metaclasses can validate that classes define certain attributes:
class ValidatedMeta(type):
required_attrs = []
def __new__(cls, name, bases, namespace):
# Skip validation for the base class
if bases:
for attr in cls.required_attrs:
if attr not in namespace:
raise TypeError(
f"Class '{name}' must define '{attr}' attribute"
)
return super().__new__(cls, name, bases, namespace)
class ModelMeta(ValidatedMeta):
required_attrs = ['table_name', 'fields']
class BaseModel(metaclass=ModelMeta):
pass
# This works: where all required attributes are defined
class User(BaseModel):
table_name = "users"
fields = ["id", "name", "email"]
print(f"User table: {User.table_name}")
print(f"User fields: {User.fields}")
Output:
User table: users
User fields: ['id', 'name', 'email']
# This raises an error: missing 'fields'
class InvalidModel(BaseModel):
table_name = "products"
# Missing 'fields' attribute!
Output:
TypeError: Class 'InvalidModel' must define 'fields' attribute
Metaclasses are powerful but add complexity. Before using a metaclass, consider whether class decorators, __init_subclass__ (Python 3.6+), or simple inheritance could achieve the same goal more simply:
# Often simpler than a metaclass:
class Base:
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
cls.registered = True
cls.plugin_name = cls.__name__.lower()
class MyPlugin(Base):
pass
print(MyPlugin.registered) # True
print(MyPlugin.plugin_name) # myplugin
Conclusion
Metaclasses give you complete control over class creation in Python, allowing you to automatically add attributes, validate class definitions, register classes, and enforce patterns across your codebase.
Add attributes by setting them in __new__ for per-class attributes, or on the metaclass itself for shared configuration.
For real-world use cases like plugin registration, ORM models, and framework design, metaclasses are a powerful tool, but always consider simpler alternatives like __init_subclass__ or class decorators first, and reach for metaclasses only when you truly need to customize the class creation process.