New Python 3 syntax for metaclasses

Metaclasses are not a new feature and have been available in Python since version 2.2. Anyway, the syntax of this changed significantly and this change is neither backward nor forward compatible. The new syntax is as follows:

class ClassWithAMetaclass(metaclass=type): 
    pass 

In Python 2, this must be written as follows:

class ClassWithAMetaclass(object): 
    __metaclass__ = type 

Class statements in Python 2 do not accept keyword arguments, so Python 3 syntax for defining metaclasses will raise the SyntaxError exception on import. It is still possible to write code using metaclasses that will run on both Python versions, but it requires some extra work. Fortunately, compatibility-related packages such as six provide simple and reusable solutions to this problem, such as the one shown in the following code:

from six import with_metaclass 
 
 
class Meta(type): 
    pass 
 
 
class Base(object): 
    pass 
 
 
class MyClass(with_metaclass(Meta, Base)): 
    pass

The other important difference is the lack of the __prepare__() hook in Python 2 metaclasses. Implementing such a function will not raise any exceptions under Python 2, but this is pointless because it will not be called in order to provide a clean namespace object. This is why packages that need to maintain Python 2 compatibility need to rely on more complex tricks if they want to achieve things that are a lot easier to implement using __prepare__(). For instance, the Django REST Framework (http://www.django-rest-framework.org) in version 3.4.7 uses the following approach to preserve the order in which attributes are added to a class:

class SerializerMetaclass(type): 
    @classmethod 
    def _get_declared_fields(cls, bases, attrs): 
        fields = [(field_name, attrs.pop(field_name)) 
                  for field_name, obj in list(attrs.items()) 
                  if isinstance(obj, Field)] 
        fields.sort(key=lambda x: x[1]._creation_counter) 
 
        # If this class is subclassing another Serializer, add  
        # that Serializer's fields.  
        # Note that we loop over the bases in *reverse*.  
        # This is necessary in order to maintain the  
        # correct order of fields. 
        for base in reversed(bases): 
            if hasattr(base, '_declared_fields'): 
                fields = list(base._declared_fields.items()) + fields 
 
        return OrderedDict(fields) 
 
    def __new__(cls, name, bases, attrs): 
        attrs['_declared_fields'] = cls._get_declared_fields( 
            bases, attrs 
        ) 
        return super(SerializerMetaclass, cls).__new__( 
            cls, name, bases, attrs 
        )

This is the workaround for the fact that the default namespace type, which is dict, does not guarantee to preserve the order of the key-value tuples in Python versions older than 3.7 (see the Dictionaries section of Chapter 3, Modern Syntax Elements – Below the Class Level). The _creation_counter attribute is expected to be in every instance of the Field class. This Field.creation_counter attribute is created in the same way as InstanceCountingClass.instance_number which was presented in the Using __new__() for overriding the instance creation process section. This is a rather complex solution that breaks a single responsibility principle by sharing its implementation across two different classes only to ensure a trackable order of attributes. In Python 3, this could be simpler because __prepare__() can return other mapping types, such as OrderedDictas shown in the following code:

from collections import OrderedDict 
 
 
class OrderedMeta(type): 
    @classmethod 
    def __prepare__(cls, name, bases, **kwargs): 
        return OrderedDict()  
    def __new__(mcs, name, bases, namespace): 
        namespace['order_of_attributes'] = list(namespace.keys()) 
        return super().__new__(mcs, name, bases, namespace) 
 
 
class ClassWithOrder(metaclass=OrderedMeta): 
    first = 8 
    second = 2

If you inspect ClassWithOrder in an interactive session, you'll see the following output: 

>>> ClassWithOrder.order_of_attributes 
['__module__', '__qualname__', 'first', 'second'] 
>>> ClassWithOrder.__dict__.keys() 
dict_keys(['__dict__', 'first', '__weakref__', 'second', 
'order_of_attributes', '__module__', '__doc__'])

The uses of metaclasses are given in the next section.