Metaclasses: Form and Usage

Robert Crowther Jan 2022
Last Modified: Feb 2023

If you want to know what metaclasses are, go look on the web. Quoting from somewhere,

Most Python programmers rarely, if ever, have to think about metaclasses

Except me. Except me. I have to think about them all the time. And I’m not a Python programmer. What I want to know is the options, the form for coding, and when when they run. Heve you ever wanted/needed to override a metaclass? I have.

Sample Code

So,

#!/usr/bin/env python3
import collections


# Note that if a class inherits a metaclass, any new metaclass must
# inherit the initial metaclass, or it is an error.
class Metaclass(type):

    # This method and name,
    # https://www.python.org/dev/peps/pep-3115/
    # Main purpose of this, base of class as ordered dict,
    # Classmethod because called before the class exists
    @classmethod
    def __prepare__(self, name, bases):
        print("{}: __prepare__ in metaclass called".format(self.__name__))
        print("  Parameters(cls, name: {}, bases: {})\n".format(self, name, bases))
        return collections.OrderedDict()

    def __new__(mcs, name, bases, attrs):
        print("{}: __new__ in metaclass called".format(mcs.__name__))
        print("  Parameters(mcs: {}, name: {}, bases: {}, attrs: {})\n".format(mcs, name, bases, attrs))
        new_class = super().__new__(mcs,  name, bases, attrs)

        # may want to do work here, with the new class, like add or
        # gather data from attributes
        return new_class

    # Does init get called?
    def __init__(self, *args, **kwargs):
        print("{}: __init___ called".format(self.__class__.__name__))


# This placement of metaclass is a later decision from
# https://www.python.org/dev/peps/pep-3115/
class Innocent(metaclass=Metaclass):

    def __new__(cls, *args, **kwargs):
        print('{}: __new__ called and returns class instance, finally from Python class "type"'.format(cls.__name__))
        print("  Parameters(cls: {}, args: {}, kwargs: {})\n".format(cls, args, kwargs))
        return super().__new__(cls)

    def __init__(self, *args, **kwargs):
        print("{}: __init___ called".format(self.__class__.__name__))

    # To avoid using metaclasses for this purpose, a hook,
    # https://www.python.org/dev/peps/pep-0487/
    def __init_subclass__(cls, **kwargs):
        print("{}: __init_subclass__ called".format(cls.__name__))
        print("  Parameters(cls: {}, kwargs: {})\n".format(cls, kwargs))
        super().__init_subclass__()

    # To avoid using metaclasses for this purpose, a hook,
    # https://www.python.org/dev/peps/pep-0487/
    def __set_name__(self, owner, name):
        print("{}: __set_name__ called".format(self.__class__.__name__))
        self.key = name


# Declaring a subclass, to illustrate various meta calls
class InnocenceLearned(Innocent):
    pass


print("\nInnocent---instanciate")
print("----------------------")
Innocent()

print("\nInnocenceLearned---instanciate")
print("------------------------------")
InnocenceLearned()

Output,

Metaclass: __prepare__ in metaclass called
  Parameters(cls, name: <class '__main__.Metaclass'>, bases: Innocent)

Metaclass: __new__ in metaclass called
  Parameters(mcs: <class '__main__.Metaclass'>, name: Innocent, bases: (), attrs: OrderedDict([('__module__', '__main__'), ('__qualname__', 'Innocent'), ('__new__', <function Innocent.__new__ at 0x7f1745bf0e18>), ('__init__', <function Innocent.__init__ at 0x7f1745bf0ea0>), ('__init_subclass__', <function Innocent.__init_subclass__ at 0x7f1745bf0f28>), ('__set_name__', <function Innocent.__set_name__ at 0x7f1745c69048>), ('__classcell__', <cell at 0x7f1745c3afd8: empty>)]))

Metaclass: __init___ called
Metaclass: __prepare__ in metaclass called
  Parameters(cls, name: <class '__main__.Metaclass'>, bases: InnocenceLearned)

Metaclass: __new__ in metaclass called
  Parameters(mcs: <class '__main__.Metaclass'>, name: InnocenceLearned, bases: (<class '__main__.Innocent'>,), attrs: OrderedDict([('__module__', '__main__'), ('__qualname__', 'InnocenceLearned')]))

InnocenceLearned: __init_subclass__ called
  Parameters(cls: <class '__main__.InnocenceLearned'>, kwargs: {})

Metaclass: __init___ called

Innocent---instanciate
----------------------
Innocent: __new__ called and returns class instance, finally from Python class "type"
  Parameters(cls: <class '__main__.Innocent'>, args: (), kwargs: {})

Innocent: __init___ called

InnocenceLearned---instanciate
------------------------------
InnocenceLearned: __new__ called and returns class instance, finally from Python class "type"
  Parameters(cls: <class '__main__.InnocenceLearned'>, args: (), kwargs: {})

InnocenceLearned: __init___ called

Notes

Order is,

And usage,

Overall, you face what I call the ‘I’m most important’ problem. Ask any part of a democratic and collaborative venture, and they will tell you they are the most important part. For sure, if you are already dealing with code where people thought it would be cool to use metaclasses, your chances of anticipating that code using an override are limited. Best you can do is try modify afterwards.

Anyway, it will do for me, and if it helps you, good.

References

Metaclasses in Python 3000, new‐style declaration and __prepare__,

https://www.python.org/dev/peps/pep-3115/

Simpler customisation of class creation, __init_subclass__ and __set_name__,

https://www.python.org/dev/peps/pep-0487/