Python Metaclass Example
Summary: in this tutorial, you’ll learn about a Python metaclass example that creates classes with many features.
Introduction to the Python metaclass example
The following defines a Person class with two attributes name and age:
class Person:
 def __init__(self, name, age):
 self.name = name
 self.age = age
 def name(self):
 return self._name
 def name(self, value):
 self._name = value
 def age(self):
 return self._age
 def age(self, value):
 self._age = value
 def __eq__(self, other):
 return self.name == other.name and self.age == other.age
 def __hash__(self):
 return hash(f'{self.name, self.age}')
 def __str__(self):
 return f'Person(name={self.name},age={self.age})'
 def __repr__(self):
 return f'Person(name={self.name},age={self.age})'
Code language: Python (python)
Typically, when defining a new class, you need to:
- Define a list of object’s properties.
- Define an __init__method to initialize object’s attributes.
- Implement the __str__and__repr__methods to represent the objects in human-readable and machine-readable formats.
- Implement the __eq__method to compare objects by values of all properties.
- Implement the __hash__method to use the objects of the class as keys of a dictionary or elements of a set.
As you can see, it requires a lot of code.
Imagine you want to define a Person class like this and automagically has all the functions above:
class Person:
 props = ['first_name', 'last_name', 'age']Code language: Python (python)
To do that, you can use a metaclass.
Define a metaclass
First, define the Data metaclass that inherits from the type class:
class Data(type):
 passCode language: Python (python)
Second, override the __new__ method to return a new class object:
class Data(type):
 def __new__(mcs, name, bases, class_dict):
 class_obj = super().__new__(mcs, name, bases, class_dict)
 return class_objCode language: Python (python)
Note that the __new__ method is a static method of the Data metaclass. And you don’t need to use the @staticmethod decorator because Python treats it special.
Also, the __new__ method creates a new class like the Person class, not the instance of the Person class.
Create property objects
First, define a Prop class that accepts an attribute name and contains three methods for creating a property object(set, get, and delete). The Data metaclass will use this Prop class for adding property objects to the class.
class Prop:
 def __init__(self, attr):
 self._attr = attr def get(self, obj):
 return getattr(obj, self._attr)
 def set(self, obj, value):
 return setattr(obj, self._attr, value)
 def delete(self, obj):
 return delattr(obj, self._attr)
Code language: Python (python)
Second, create a new static method define_property() that creates a property object for each attribute from the props list:
class Data(type):
 def __new__(mcs, name, bases, class_dict):
 class_obj = super().__new__(mcs, name, bases, class_dict)
 Data.define_property(class_obj) return class_obj
 def define_property(class_obj):
 for prop in class_obj.props:
 attr = f'_{prop}'
 prop_obj = property(
 fget=Prop(attr).get,
 fset=Prop(attr).set,
 fdel=Prop(attr).delete
 )
 setattr(class_obj, prop, prop_obj)
 return class_obj
Code language: Python (python)
The following defines the Person class that uses the Data metaclass:
class Person(metaclass=Data):
 props = ['name', 'age']Code language: Python (python)
The Person class has two properties name and age:
pprint(Person.__dict__)Code language: Python (python)
Output:
mappingproxy({'__dict__': <attribute '__dict__' of 'Person' objects>,
 '__doc__': None,
 '__module__': '__main__',
 '__weakref__': <attribute '__weakref__' of 'Person' objects>,
 'age': <property object at 0x000002213CA92090>,
 'name': <property object at 0x000002213C772A90>,
 'props': ['name', 'age']})Code language: Python (python)
Define __init__ method
The following defines an init static method and assign it to the __init__ attribute of the class object:
class Data(type):
 def __new__(mcs, name, bases, class_dict):
 class_obj = super().__new__(mcs, name, bases, class_dict) # create property
 Data.define_property(class_obj)
 # define __init__
 setattr(class_obj, '__init__', Data.init(class_obj))
 return class_obj
 def init(class_obj):
 def _init(self, *obj_args, **obj_kwargs):
 if obj_kwargs:
 for prop in class_obj.props:
 if prop in obj_kwargs.keys():
 setattr(self, prop, obj_kwargs[prop])
 if obj_args:
 for kv in zip(class_obj.props, obj_args):
 setattr(self, kv[0], kv[1])
 return _init
 # more methods
Code language: Python (python)
The following creates a new instance of the Person class and initialize its attributes:
p = Person('John Doe', age=25)
 print(p.__dict__)Code language: Python (python)
Output:
{'_age': 25, '_name': 'John Doe'}Code language: Python (python)
The p.__dict__ contains two attributes _name and _age based on the predefined names in the props list.
Define __repr__ method
The following defines the repr static method that returns a function and uses it for the __repr__ attribute of the class object:
class Data(type):
 def __new__(mcs, name, bases, class_dict):
 class_obj = super().__new__(mcs, name, bases, class_dict) # create property
 Data.define_property(class_obj)
 # define __init__
 setattr(class_obj, '__init__', Data.init(class_obj))
 # define __repr__
 setattr(class_obj, '__repr__', Data.repr(class_obj))
 return class_obj
 def repr(class_obj):
 def _repr(self):
 prop_values = (getattr(self, prop) for prop in class_obj.props)
 prop_key_values = (f'{key}={value}' for key, value in zip(class_obj.props, prop_values))
 prop_key_values_str = ', '.join(prop_key_values)
 return f'{class_obj.__name__}({prop_key_values_str})'
 return _repr
Code language: Python (python)
The following creates a new instance of the Person class and displays it:
p = Person('John Doe', age=25)
 print(p)Code language: Python (python)
Output:
Person(name=John Doe, age=25)Code language: Python (python)
Define __eq__ and __hash__ methods
The following defines the eq and hash methods and assigns them to the __eq__ and __hash__ of the class object of the metaclass:
class Data(type):
 def __new__(mcs, name, bases, class_dict):
 class_obj = super().__new__(mcs, name, bases, class_dict) # create property
 Data.define_property(class_obj)
 # define __init__
 setattr(class_obj, '__init__', Data.init(class_obj))
 # define __repr__
 setattr(class_obj, '__repr__', Data.repr(class_obj))
 # define __eq__ & __hash__
 setattr(class_obj, '__eq__', Data.eq(class_obj))
 setattr(class_obj, '__hash__', Data.hash(class_obj))
 return class_obj
 def eq(class_obj):
 def _eq(self, other):
 if not isinstance(other, class_obj):
 return False
 self_values = [getattr(self, prop) for prop in class_obj.props]
 other_values = [getattr(other, prop) for prop in other.props]
 return self_values == other_values
 return _eq
 def hash(class_obj):
 def _hash(self):
 values = (getattr(self, prop) for prop in class_obj.props)
 return hash(tuple(values))
 return _hash
Code language: Python (python)
The following creates two instances of the Person and compares them. If the values of all properties are the same, they will be equal. Otherwise, they will not be equal:
p1 = Person('John Doe', age=25)
 p2 = Person('Jane Doe', age=25)print(p1 == p2) # False
p2.name = 'John Doe'
 print(p1 == p2) # True
Code language: Python (python)
Put it all together
from pprint import pprintclass Prop:
 def __init__(self, attr):
 self._attr = attr
 def get(self, obj):
 return getattr(obj, self._attr)
 def set(self, obj, value):
 return setattr(obj, self._attr, value)
 def delete(self, obj):
 return delattr(obj, self._attr)
class Data(type):
 def __new__(mcs, name, bases, class_dict):
 class_obj = super().__new__(mcs, name, bases, class_dict)
 # create property
 Data.define_property(class_obj)
 # define __init__
 setattr(class_obj, '__init__', Data.init(class_obj))
 # define __repr__
 setattr(class_obj, '__repr__', Data.repr(class_obj))
 # define __eq__ & __hash__
 setattr(class_obj, '__eq__', Data.eq(class_obj))
 setattr(class_obj, '__hash__', Data.hash(class_obj))
 return class_obj
 def eq(class_obj):
 def _eq(self, other):
 if not isinstance(other, class_obj):
 return False
 self_values = [getattr(self, prop) for prop in class_obj.props]
 other_values = [getattr(other, prop) for prop in other.props]
 return self_values == other_values
 return _eq
 def hash(class_obj):
 def _hash(self):
 values = (getattr(self, prop) for prop in class_obj.props)
 return hash(tuple(values))
 return _hash
 def repr(class_obj):
 def _repr(self):
 prop_values = (getattr(self, prop) for prop in class_obj.props)
 prop_key_values = (f'{key}={value}' for key, value in zip(class_obj.props, prop_values))
 prop_key_values_str = ', '.join(prop_key_values)
 return f'{class_obj.__name__}({prop_key_values_str})'
 return _repr
 def init(class_obj):
 def _init(self, *obj_args, **obj_kwargs):
 if obj_kwargs:
 for prop in class_obj.props:
 if prop in obj_kwargs.keys():
 setattr(self, prop, obj_kwargs[prop])
 if obj_args:
 for kv in zip(class_obj.props, obj_args):
 setattr(self, kv[0], kv[1])
 return _init
 def define_property(class_obj):
 for prop in class_obj.props:
 attr = f'_{prop}'
 prop_obj = property(
 fget=Prop(attr).get,
 fset=Prop(attr).set,
 fdel=Prop(attr).delete
 )
 setattr(class_obj, prop, prop_obj)
 return class_obj
class Person(metaclass=Data):
 props = ['name', 'age']
if __name__ == '__main__':
 pprint(Person.__dict__)
 p1 = Person('John Doe', age=25)
 p2 = Person('Jane Doe', age=25)
 print(p1 == p2) # False
 p2.name = 'John Doe'
 print(p1 == p2) # True
Code language: Python (python)
Decorator
The following defines a class called Employee that uses the Data metaclass:
class Employee(metaclass=Data):
 props = ['name', 'job_title']if __name__ == '__main__':
 e = Employee(name='John Doe', job_title='Python Developer')
 print(e)
Code language: Python (python)
Output:
Employee(name=John Doe, job_title=Python Developer)Code language: Python (python)
It works as expected. However, specifying the metaclass is quite verbose. To improve this, you can use a function decorator.
First, define a function decorator that returns a new class which is an instance of the Data metaclass:
def data(cls):
 return Data(cls.__name__, cls.__bases__, dict(cls.__dict__))Code language: Python (python)
Second, use the @data decorator for any class that uses the Data as the metaclass:
 class Employee:
 props = ['name', 'job_title']Code language: Python (python)
The following shows the complete code:
class Prop:
 def __init__(self, attr):
 self._attr = attr def get(self, obj):
 return getattr(obj, self._attr)
 def set(self, obj, value):
 return setattr(obj, self._attr, value)
 def delete(self, obj):
 return delattr(obj, self._attr)
class Data(type):
 def __new__(mcs, name, bases, class_dict):
 class_obj = super().__new__(mcs, name, bases, class_dict)
 # create property
 Data.define_property(class_obj)
 # define __init__
 setattr(class_obj, '__init__', Data.init(class_obj))
 # define __repr__
 setattr(class_obj, '__repr__', Data.repr(class_obj))
 # define __eq__ & __hash__
 setattr(class_obj, '__eq__', Data.eq(class_obj))
 setattr(class_obj, '__hash__', Data.hash(class_obj))
 return class_obj
 def eq(class_obj):
 def _eq(self, other):
 if not isinstance(other, class_obj):
 return False
 self_values = [getattr(self, prop) for prop in class_obj.props]
 other_values = [getattr(other, prop) for prop in other.props]
 return self_values == other_values
 return _eq
 def hash(class_obj):
 def _hash(self):
 values = (getattr(self, prop) for prop in class_obj.props)
 return hash(tuple(values))
 return _hash
 def repr(class_obj):
 def _repr(self):
 prop_values = (getattr(self, prop) for prop in class_obj.props)
 prop_key_values = (f'{key}={value}' for key, value in zip(class_obj.props, prop_values))
 prop_key_values_str = ', '.join(prop_key_values)
 return f'{class_obj.__name__}({prop_key_values_str})'
 return _repr
 def init(class_obj):
 def _init(self, *obj_args, **obj_kwargs):
 if obj_kwargs:
 for prop in class_obj.props:
 if prop in obj_kwargs.keys():
 setattr(self, prop, obj_kwargs[prop])
 if obj_args:
 for kv in zip(class_obj.props, obj_args):
 setattr(self, kv[0], kv[1])
 return _init
 def define_property(class_obj):
 for prop in class_obj.props:
 attr = f'_{prop}'
 prop_obj = property(
 fget=Prop(attr).get,
 fset=Prop(attr).set,
 fdel=Prop(attr).delete
 )
 setattr(class_obj, prop, prop_obj)
 return class_obj
class Person(metaclass=Data):
 props = ['name', 'age']
def data(cls):
 return Data(cls.__name__, cls.__bases__, dict(cls.__dict__))
 class Employee:
 props = ['name', 'job_title']
Code language: Python (python)
Python 3.7 provided a @dataclass decorator specified in the PEP 557 that has some features like the Data metaclass. Also, the dataclass offers more features that help you save time when working with classes.