Python classes are objects too. Just as a class describes how its instances behave, a metaclass describes how a class is constructed. Metaclasses are useful when a framework needs to consistently validate or modify many class definitions, but they add complexity and are rarely needed for ordinary application logic.
What is a metaclass?
For a typical class, the relationship is:
Employeeis a class.employeeis an instance ofEmployee.type(Employee)istype, Python's default metaclass for classes.
class Employee:
pass
employee = Employee()
print(type(employee))
# <class '__main__.Employee'>
print(type(Employee))
# <class 'type'>
The phrase “a class of a class” is a useful shorthand: type is the class that creates ordinary classes, and Employee creates employee instances.
Create a class with type
The three-argument form of type creates a class dynamically. It takes the class name, a tuple of base classes, and a dictionary of attributes:
Employee = type(
"Employee",
(),
{"company": "Example Corp"},
)
print(Employee.company)
# Example Corp
This is similar in spirit to a simple class statement, but class statements are more readable for most code. Custom metaclasses let you customize the class-construction process itself.
Write a custom metaclass
A custom metaclass usually subclasses type. Its __new__ method receives the metaclass, the new class's name, its base classes, and the namespace containing its attributes. It can inspect or adjust that namespace before delegating class creation to type:
class LoggingMeta(type):
def __new__(mcls, name, bases, namespace):
print(f"Creating class: {name}")
return super().__new__(mcls, name, bases, namespace)
class Employee(metaclass=LoggingMeta):
pass
# Creating class: Employee
The hook is named __new__ with two leading and two trailing underscores. A method named new is just an ordinary method and will not intercept class creation.
Add a method to classes
A metaclass can add a default method to every class that uses it. setdefault preserves a class's own implementation when it already defines that method:
class GreetingMeta(type):
def __new__(mcls, name, bases, namespace):
def greet(self):
return "Hello"
namespace.setdefault("greet", greet)
return super().__new__(mcls, name, bases, namespace)
class Employee(metaclass=GreetingMeta):
pass
print(Employee().greet())
# Hello
Transform class attributes
A metaclass can rewrite the namespace before class creation. This example uppercases non-dunder attribute names while leaving Python's special attributes unchanged:
class UpperAttrsMeta(type):
def __new__(mcls, name, bases, namespace):
updated = {
key if key.startswith("__") else key.upper(): value
for key, value in namespace.items()
}
return super().__new__(mcls, name, bases, updated)
class Person(metaclass=UpperAttrsMeta):
city = "Hyderabad"
print(Person.CITY)
# Hyderabad
This demonstrates namespace rewriting, but renaming attributes can surprise users and break expected APIs. Apply transformations only when the convention is explicit and consistently enforced.
Validate class definitions
Frameworks can use a metaclass to reject an invalid class as it is being defined. This example requires each concrete model to declare its own table_name:
class ModelMeta(type):
def __new__(mcls, name, bases, namespace):
if name != "BaseModel" and "table_name" not in namespace:
raise TypeError(f"{name} must define table_name")
return super().__new__(mcls, name, bases, namespace)
class BaseModel(metaclass=ModelMeta):
pass
class User(BaseModel):
table_name = "users"
class Product(BaseModel):
pass # Raises TypeError during class definition
This check deliberately requires an attribute in each subclass's own namespace. A real framework should document whether inherited values count and whether abstract base classes are exempt.
Register plugin classes automatically
Metaclasses can collect related classes in a registry as they are defined. A base class can opt out of registration while concrete plugins are recorded under explicit names:
class PluginMeta(type):
registry = {}
def __new__(mcls, name, bases, namespace):
plugin_class = super().__new__(mcls, name, bases, namespace)
if not namespace.get("_abstract", False):
plugin_name = namespace.get("plugin_name", name)
mcls.registry[plugin_name] = plugin_class
return plugin_class
class PluginBase(metaclass=PluginMeta):
_abstract = True
class JsonPlugin(PluginBase):
plugin_name = "json"
class CsvPlugin(PluginBase):
plugin_name = "csv"
print(sorted(PluginMeta.registry))
# ['csv', 'json']
In production, define how duplicate names, abstract classes, imports, and plugin initialization are handled. For many projects, an explicit registration function is easier to understand and debug.
Metaclass or class decorator?
A class decorator can modify a class after its creation, which is often enough for a one-off addition:
def add_greeting(cls):
def greet(self):
return "Hello"
cls.greet = greet
return cls
@add_greeting
class Employee:
pass
- Use a decorator to customize one class or apply a simple transformation after creation.
- Use
__init_subclass__when a base class needs to validate or register its subclasses. - Use a metaclass when the class-creation process itself needs coordinated control across a framework or multiple inheritance hierarchies.
When should you use a metaclass?
Metaclasses can be appropriate for ORMs, validation frameworks, plugin architectures, dependency-injection systems, or other framework-level features that must inspect many class definitions consistently. They can enforce conventions, add methods, or populate registries automatically.
Avoid them for ordinary business rules, simple APIs, small scripts, or data processing that a function, decorator, class factory, or __init_subclass__ can handle. Metaclasses can interact with inheritance and other metaclasses, so a custom metaclass may also create compatibility conflicts. Prefer the least powerful mechanism that clearly solves the problem.
Remember: type creates ordinary classes; a custom metaclass customizes that creation process. Reach for one when a framework needs consistent class-level behavior, not just because it is possible.