Skip to main content

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

ParameterDescription
clsThe metaclass itself (MyMeta)
nameThe name of the class being created ('MyClass')
basesTuple of base classes (parent classes)
namespaceDictionary 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
Metaclass attributes vs class attributes

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
Use metaclasses sparingly

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.