8.6. Metaprogramming Metaclass

  • Object is an instance of a class

  • Class is an instance of a Metaclass

  • Class defines how an object behaves

  • Metaclass defines how a class behaves

Metaclasses are deeper magic than 99% of users should ever worry about. If you wonder whether you need them, you don't. The people who actually need them know with certainty that they need them, and don't need an explanation about why.

—Tim Peters

../../_images/oop-metaclass-inheritance.png

Figure 8.2. Object is an instance of a Class. Class is an instance of a Metaclass. Metaclass is an instance of a type. Type is an instance of a type.

When a class definition is executed, the following steps occur:

  1. MRO entries are resolved;

  2. the appropriate metaclass is determined;

  3. the class namespace is prepared;

  4. the class body is executed;

  5. the class object is created.

When using the default metaclass type, or any metaclass that ultimately calls type.__new__, the following additional customisation steps are invoked after creating the class object:

  1. type.__new__ collects all of the descriptors in the class namespace that define a __set_name__() method;

  2. all of these __set_name__ methods are called with the class being defined and the assigned name of that particular descriptor;

  3. the __init_subclass__() hook is called on the immediate parent of the new class in its method resolution order. [2]

8.6.1. Class Based Metaclass

  • Typically Metaclasses are defined as a class

  • Metaclass is a class which returns a class from it's constructor

class MyType(type):
    def __new__(metacls, clsname, bases, attrs):
        return type(clsname, bases, attrs)

Then we can use it, instead of a regular type():

User = MyType('User', (), {})

8.6.2. Using Metaclasses

  • Metaclasses are used by setting the metaclass keyword argument

  • This works for both function and class based metaclasses

  • For future, we will use the class based metaclasses and metaclass=... syntax

  • It is exactly equivalent to other ones, but it is more common in practice

Class based metaclass:

class MyType(type):
    def __new__(metacls, clsname, bases, attrs):
        return type(clsname, bases, attrs)

class User(metaclass=MyType):
    pass

8.6.3. Rationale

  • Metaclasses allow you to do 'extra things' when creating a class

  • Allow customization of class instantiation

  • Most commonly used as a class-factory

  • Registering the new class with some registry

  • Replace the class with something else entirely

  • Inject logger instance

  • Injecting static fields

  • Ensure subclass implementation

  • Metaclasses run when Python defines class (even if no instance is created)

The potential uses for metaclasses are boundless. Some ideas that have been explored include enum, logging, interface checking, automatic delegation, automatic property creation, proxies, frameworks, and automatic resource locking/synchronization. [2]

class MyType(type):
    def __new__(metacls, clsname, bases, attrs):
        print('before')
        cls = super().__new__(metacls, clsname, bases, attrs)
        print('after')
        return cls
class User(metaclass=MyType):
    pass

before
after

Metaclasses allow you to do 'extra things' when creating a class. You can do something before, or after class creation. You can modify a class, add new fields or methods, ensure name is correct, ensure it inherits from some other class, register it in some registry such as event listener, inject named logger instance, or even replace it with something else entirely.

8.6.4. Example - 1

  • Ensure class name starts with uppercase letter

  • I have chosen NameError exception, but you can choose any other

import string


class MyType(type):
    def __new__(metacls, clsname, bases, attrs):
        lowercase_letters = tuple(string.ascii_lowercase)
        if clsname.startswith(lowercase_letters):
            raise NameError('Class name must start with uppercase letter')
        return super().__new__(metacls, clsname, bases, attrs)

This code will work without any problems:

class User(metaclass=MyType):
    pass

But if we change a name of a class to starts with lowercase u, then we will receive an exception:

class user(metaclass=MyType):
    pass

Traceback (most recent call last):
NameError: Class name must start with uppercase letter

8.6.5. Example - 2

  • Ensure class inherits from some other class

  • I have chosen TypeError exception, but you can choose any other

class Account:
    pass
class MyType(type):
    def __new__(metacls, clsname, bases, attrs):
        if Account not in bases:
            raise TypeError('All classes must inherit from Account')
        return super().__new__(metacls, clsname, bases, attrs)

This code will work without any problems:

class User(Account, metaclass=MyType):
    pass

If we forget to inherit from Account class, then we will receive an exception:

class User(metaclass=MyType):
    pass

Traceback (most recent call last):
TypeError: All classes must inherit from Account

8.6.6. Example - 3

  • Ensure __init__ method is always present in a class

  • I have chosen TypeError exception, but you can choose any other

class MyType(type):
    def __new__(metacls, clsname, bases, attrs):
        if '__init__' not in attrs:
            raise TypeError('All classes must have __init__ method')
        return super().__new__(metacls, clsname, bases, attrs)

This code will work without any problems:

class User(metaclass=MyType):
    def __init__(self):
        pass

If we forget to add __init__ method, then we will receive an exception:

class User(metaclass=MyType):
    pass

Traceback (most recent call last):
TypeError: All classes must have __init__ method

8.6.7. Hiding Metaclass

  • Metaclasses can be hidden by using inheritance

  • This is a common pattern in Python frameworks

class MyType(type):
    def __new__(metacls, name, bases, attrs):
        uppercase = tuple(string.ascii_uppercase)
        if not name.startswith(uppercase):
            raise ValueError(f'Class name must start with uppercase letter')
        cls = super().__new__(metacls, name, bases, attrs)
        return cls

class Account(metaclass=MyType):
    pass

Usage:

class User(Account):
    pass
class user(Account):
    pass

Traceback (most recent call last):
ValueError: Class name must start with uppercase letter

8.6.8. Metaclass Arguments

  • Metaclasses can accept additional arguments

  • Arguments can be passed to metaclass using metaclass=... syntax

  • This behavior allows for more customization

from pprint import pprint
import logging
from datetime import datetime, timezone
from uuid import uuid4
class MyType(type):
    def __new__(metacls, clsname, bases, attrs, debug=False):
        cls = super().__new__(metacls, clsname, bases, attrs)
        if debug:
            cls._since = datetime.now(timezone.utc)
            cls._uuid = uuid4()
            cls._log = logging.getLogger(cls.__name__)
        return cls

Creating an object without additional arguments, will create a normal class with typical attributes:

class User(metaclass=MyType):
    pass

pprint(vars(User))
mappingproxy({'__dict__': <attribute '__dict__' of 'User' objects>,
              '__doc__': None,
              '__firstlineno__': 1,
              '__module__': '__main__',
              '__static_attributes__': (),
              '__weakref__': <attribute '__weakref__' of 'User' objects>})

Setting debug to True, will create a class with additional attributes, that is _since, _uuid, and _log, which can enable better debugging:

class User(metaclass=MyType, debug=True):
    pass

pprint(vars(User))
mappingproxy({'__dict__': <attribute '__dict__' of 'User' objects>,
              '__doc__': None,
              '__module__': '__main__',
              '__weakref__': <attribute '__weakref__' of 'User' objects>,
              '_log': <Logger User (INFO)>,
              '_since': datetime.datetime(2024, 8, 21, 10, 11, 16, 244761, tzinfo=datetime.timezone.utc),
              '_uuid': UUID('8eb20432-38dc-4a3b-b798-079285fabde0')})

8.6.9. Metaclass Protocol

  • __prepare__(metacls, name, bases, **kwargs) -> dict - on class namespace initialization

  • __new__(metacls, classname, bases, attrs) -> cls - before class creation

  • __init__(self, name, bases, attrs) -> None - after class creation

  • __call__(self, *args, **kwargs) - when the class is called (instantiated)

class MyType(type):
    @classmethod
    def __prepare__(metacls, name, bases) -> 'mappingproxy': ...
    def __new__(metacls, classname, bases, attrs, *args, **kwargs) -> type: ...
    def __init__(self, *args, **kwargs) -> None: ...
    def __call__(self, *args, **kwargs) -> object: ...

Once the appropriate metaclass has been identified, then the class namespace is prepared. If the metaclass has a __prepare__ attribute, it is called as namespace = metaclass.__prepare__(name, bases, **kwds) (where the additional keyword arguments, if any, come from the class definition). The __prepare__ method should be implemented as a classmethod(). The namespace returned by __prepare__ is passed in to __new__, but when the final class object is created the namespace is copied into a new dict. If the metaclass has no __prepare__ attribute, then the class namespace is initialised as an empty ordered mapping. [1]

class MyType(type):
    @classmethod
    def __prepare__(metacls, name, bases):
        print('prepare: before')
        result = super().__prepare__(metacls, name, bases)
        print('prepare: after')
        return result

    def __new__(metacls, clsname, bases, attrs):
        print('new: before')
        result = super().__new__(metacls, clsname, bases, attrs)
        print('new: after')
        return result

    def __init__(self, *args, **kwargs):
        print('init: before')
        super().__init__(*args, **kwargs)
        print('init: after')

    def __call__(self, *args, **kwargs):
        print('call: before')
        result = super().__call__(*args, **kwargs)
        print('call: after')
        return result
class User(metaclass=MyType):
    pass
prepare: before
prepare: after
new: before
new: after
init: before
init: after
mark = User()
call: before
call: after

8.6.10. Case Study

class MyType(type):
    def __new__(metacls, clsname, bases, attrs):
        if Account not in bases:
            raise TypeError('All classes must inherit from Account')
        return super().__new__(metacls, clsname, bases, attrs)


class Account:
    pass

class User(Account, metaclass=MyType):
    pass

class Admin(metaclass=MyType):
    pass
# Traceback (most recent call last):
# TypeError: All classes must inherit from Account
from string import ascii_uppercase

class MyType(type):
    def __new__(metacls, clsname, bases, attrs):
        uppercase = tuple(ascii_uppercase)
        if not clsname.startswith(uppercase):
            raise NameError('All class names must starts with uppercase letter')
        return super().__new__(metacls, clsname, bases, attrs)


class User(metaclass=MyType):
    pass

class admin(metaclass=MyType):
    pass
# Traceback (most recent call last):
# NameError: All class names must starts with uppercase letter
class MyType(type):
    def __new__(metacls, clsname, bases, attrs):
        if ('AGE_MIN' not in attrs) or ('AGE_MAX' not in attrs):
            raise TypeError('All classes must have AGE_MIN and AGE_MAX defined')
        return super().__new__(metacls, clsname, bases, attrs)


class User(metaclass=MyType):
    AGE_MIN = 18
    AGE_MAX = 65


class Admin(metaclass=MyType):
    pass
# Traceback (most recent call last):
# TypeError: All classes must have AGE_MIN and AGE_MAX defined
import logging
from datetime import timezone, datetime
from uuid import uuid4


class MyType(type):
    def __new__(metacls, clsname, bases, attrs, trace=False):
        cls = super().__new__(metacls, clsname, bases, attrs)
        if trace:
            cls._uuid4 = uuid4()
            cls._since = datetime.now(timezone.utc)
            cls._log = logging.getLogger(clsname)
        return cls


class User(metaclass=MyType):
    pass

class Admin(metaclass=MyType, trace=True):
    pass


pprint(vars(User))
# mappingproxy({'__dict__': <attribute '__dict__' of 'User' objects>,
#               '__doc__': None,
#               '__module__': '__main__',
#               '__weakref__': <attribute '__weakref__' of 'User' objects>})


pprint(vars(Admin))
# mappingproxy({'__dict__': <attribute '__dict__' of 'Admin' objects>,
#               '__doc__': None,
#               '__module__': '__main__',
#               '__weakref__': <attribute '__weakref__' of 'Admin' objects>,
#               '_log': <Logger Admin (INFO)>,
#               '_since': datetime.datetime(2024, 8, 7, 8, 15, 58, 999871, tzinfo=datetime.timezone.utc),
#               '_uuid4': UUID('9b7bedd0-51d8-44bc-93bb-85fc298299d1')})
from datetime import datetime, timezone
from uuid import uuid4


class Register(type):
    listeners = {}

    @classmethod
    def on_create(cls, *names):
        def wrapper(func):
            for name in names:
                if name not in cls.listeners:
                    cls.listeners[name] = []
                cls.listeners[name] += [func]
        return wrapper

    def __new__(cls, name, bases, attrs):
        for listener in cls.listeners.get(name, []):
            name, bases, attrs = listener(name, bases, attrs)
        return super().__new__(cls, name, bases, attrs)


@Register.on_create('User', 'Admin')
def debug(name, bases, attrs):
    print(f'DEBUG: {name=}')
    print(f'DEBUG: {bases=}')
    print(f'DEBUG: {attrs=}')
    return name, bases, attrs

@Register.on_create('User', 'Admin')
def debug(name, bases, attrs):
    print(f'INFO: {name=}')
    return name, bases, attrs

@Register.on_create('User')
def trace(name, bases, attrs):
    attrs['_uuid4'] = uuid4()
    attrs['_since'] = datetime.now(timezone.utc)
    return name, bases, attrs


class Account(metaclass=Register):
    pass


class User(Account):
    pass
# DEBUG: name='User'
# DEBUG: bases=(<class '__main__.Account'>,)
# DEBUG: attrs={'__module__': '__main__', '__qualname__': 'User'}
# INFO: name='User'


class Admin(Account):
    pass
# DEBUG: name='Admin'
# DEBUG: bases=(<class '__main__.Account'>,)
# DEBUG: attrs={'__module__': '__main__', '__qualname__': 'Admin'}
# INFO: name='Admin'


pprint(vars(User))
# mappingproxy({'__doc__': None,
#               '__module__': '__main__',
#               '_since': datetime.datetime(2024, 8, 21, 10, 1, 58, 442815, tzinfo=datetime.timezone.utc),
#               '_uuid4': UUID('dd17cdb6-6aec-4afb-9434-54f4297715af')})


pprint(vars(Admin))
# mappingproxy({'__module__': '__main__', '__doc__': None})



# listeners = {
#     'User': [debug, trace],
#     'Admin': [debug],
# }

8.6.11. Use Case - 1

  • Injecting a named logger instance

import logging


class Logger(type):
    def __init__(cls, *args, **kwargs):
        cls._logger = logging.getLogger(cls.__name__)
class User(metaclass=Logger):
    pass

class Admin(metaclass=Logger):
    pass
print(User._logger)
<Logger User (WARNING)>

print(Admin._logger)
<Logger Admin (WARNING)>

8.6.12. Instance vs. Class vs. Metaclass

  • MRO - Method Resolution Order

  • MRO is a list of classes that Python will use to search for methods

  • MRO is defined by C3 Linearization algorithm

  • MRO is defined by __mro__ attribute

Let's consider a normal (without a metaclass) class:

class User:
    pass

Object is an instance of a class, and an object:

mark = User()

isinstance(mark, User)
True

isinstance(mark, object)
True

This is because User class is an instance of a type class:

User.__mro__
(<class '__main__.User'>, <class 'object'>)

Now, consider a case with using metaclasses:

class MyType(type):
    pass

class User(metaclass=MyType):
    pass

Instances for User class are still instances of a User class:

mark = User()

isinstance(mark, User)
True

isinstance(mark, object)
True

But, they are not an instances of a metaclass:

isinstance(mark, MyType)
False

isinstance(User, MyType)
True

And a class still inherits from object:

User.__mro__
(<class '__main__.User'>, <class 'object'>)

The following diagram displays the relationship between an instance, class and a metaclass:

../../_images/oop-metaclass-diagram.png

Figure 8.3. Object is an instance of a Class. Class is an instance of a Metaclass. Metaclass is an instance of a type. Type is an instance of a type.

8.6.13. Class Bases

  • Bases for an instance stays the same

  • Bases of a class changes when using a metaclass

class User:
    pass


User.__class__.__bases__
(<class 'object'>,)

mark = User()
mark.__class__.__bases__
(<class 'object'>,)

Bases for an instance stays the same, but note, how bases of a class changes when using a metaclass:

class MyType(type):
    def __new__(metacls, classname, bases, attrs):
        return type(classname, bases, attrs)

class User(metaclass=MyType):
    pass


User.__class__.__bases__
(<class 'object'>,)

mark = User()
mark.__class__.__bases__
(<class 'object'>,)

8.6.14. Metaclass replacements

  • Metaclasses allows for more customization, but at a cost of complexity

  • There are several ways, how you can achieve the same effect

SetUp:

import logging

Inheritance and __init__() method. This requires calling super() in a subclass if __init__() is overridden:

class Logger:
    def __init__(self):
        self._logger = logging.getLogger(self.__class__.__name__)

class User(Logger):
    pass


mark = User()
print(mark._logger)
<Logger User (WARNING)>

Inheritance and __new__() method. Overriding __new__ method is less common, but it is possible. In such case, super() is needed:

class Logger:
    def __new__(cls, *args, **kwargs):
        obj = super().__new__(cls)
        obj._logger = logging.getLogger(obj.__class__.__name__)
        return obj

class User(Logger):
    pass


mark = User()
print(mark._logger)
<Logger User (WARNING)>

Class Decorator. This is a more common way to achieve the same effect. The problem with decorators is that they substitute the class with a new class Wrapper, which can lead to problems with inheritance:

def logger(cls):
    class Wrapper(cls):
        _logger = logging.getLogger(cls.__name__)
    return Wrapper


@logger
class User:
    pass


mark = User()
print(mark._logger)
<Logger User (WARNING)>

8.6.15. Case Study

import logging
from datetime import datetime, timezone
from uuid import uuid4

Let's create a metaclass EventListener which enables to register listeners for class creation.

class OnCreate(type):
    listeners = {}

    def __new__(metacls, clsname, bases, attrs):
        for listener in metacls.listeners.get(clsname, []):
            clsname, bases, attrs = listener(clsname, bases, attrs)
        return super().__new__(metacls, clsname, bases, attrs)

    @classmethod
    def of(metacls, *clsnames):
        def wrapper(func):
            for clsname in clsnames:
                if clsname not in metacls.listeners:
                    metacls.listeners[clsname] = []
                metacls.listeners[clsname] += [func]
        return wrapper
@OnCreate.of('User')
def trace(clsname, bases, attrs):
    attrs['_since'] = datetime.now(timezone.utc)
    attrs['_uuid'] = uuid4()
    attrs['_log'] = logging.getLogger(clsname)
    return clsname, bases, attrs

@OnCreate.of('User', 'Admin')
def info(clsname, bases, attrs):
    print(f'Info: {clsname=}')
    return clsname, bases, attrs

@OnCreate.of('User', 'Admin')
def debug(clsname, bases, attrs):
    print(f'Debug: {clsname=}')
    print(f'Debug: {bases=}')
    print(f'Debug: {attrs=}')
    return clsname, bases, attrs

We can inspect what listeners are already registered to class creations:

result = OnCreate.listeners
pprint(result, width=30)
{'Admin': [<function info at 0x14c9db740>,
           <function debug at 0x14c9db7e0>],
 'User': [<function trace at 0x14c9db6a0>,
          <function info at 0x14c9db740>,
          <function debug at 0x14c9db7e0>]}

Let's create a Base class to hide the metaclass complexity:

class Base(metaclass=OnCreate):
    pass

Finally, let's create classes:

class User(Base):
    pass

Info: clsname='User'
Debug: clsname='User'
Debug: bases=(<class '__main__.Base'>,)
Debug: attrs={'__module__': '__main__', '__qualname__': 'User', '_since': datetime.datetime(2024, 7, 9, 19, 30, 3, 181585, tzinfo=datetime.timezone.utc), '_uuid': UUID('a20292f4-23c4-4a76-934b-1f2c3fd4967c'), '_log': <Logger User (INFO)>}
class Admin(Base):
    pass

Info: clsname='Admin'
Debug: clsname='Admin'
Debug: bases=(<class '__main__.Base'>,)
Debug: attrs={'__module__': '__main__', '__qualname__': 'Admin', '__firstlineno__': 1, '__static_attributes__': ()}

Now we can inspect created classes:

pprint(vars(User))
mappingproxy({'__doc__': None,
              '__module__': '__main__',
              '_log': <Logger User (INFO)>,
              '_since': datetime.datetime(2024, 7, 9, 19, 30, 3, 181585, tzinfo=datetime.timezone.utc),
              '_uuid': UUID('a20292f4-23c4-4a76-934b-1f2c3fd4967c')})
pprint(vars(Admin))
mappingproxy({'__doc__': None,
              '__firstlineno__': 1,
              '__module__': '__main__',
              '__static_attributes__': ()})

8.6.16. Use Case - 1

  • Abstract Base Classes are great example of using metaclasses

  • ABCMeta will ensure, that you cannot create an instance of an abstract class

  • ABCMeta at creation ensures that the implementing class covers all abstract methods

SetUp:

from abc import ABCMeta, abstractmethod

ABCMeta will ensure, that you cannot create an instance of an abstract class:

class Account(metaclass=ABCMeta):
    @abstractmethod
    def login(self):
        ...


mark = Account()
Traceback (most recent call last):
TypeError: Can't instantiate abstract class Account without an implementation for abstract method 'login'

ABCMeta at creation ensures that the implementing class covers all abstract methods: If not, then an exception is raised:

class Account(metaclass=ABCMeta):
    @abstractmethod
    def login(self):
        ...

class User(Account):
    pass


mark = User()
Traceback (most recent call last):
TypeError: Can't instantiate abstract class User without an implementation for abstract method 'login'

If those two conditions are met, the instance will be created:

class Account(metaclass=ABCMeta):
    @abstractmethod
    def login(self):
        ...

class User(Account):
    def login(self):
        print('User login')


mark = User()
mark.login()
User login

8.6.17. Use Case - 2

  • Metaclasses can force a class to inherit from a base class

  • They can automatically inject a base class to the bases

class Account:
    pass

class MyType(type):
    def __new__(metacls, clsname, bases, attrs):
        if Account not in bases:
            bases += (Account,)
        cls = type(clsname, bases, attrs)
        return cls

Define a class:

class User(metaclass=MyType):
    pass


User.mro()
[<class '__main__.User'>, <class '__main__.Account'>, <class 'object'>]

8.6.18. Use Case - 3

  • Event Listener

class EventListener(type):
    listeners = {}

    @classmethod
    def register(metacls, *clsnames):
        def wrapper(func):
            for clsname in clsnames:
                if not clsname in metacls.listeners:
                    metacls.listeners[clsname] = []
                metacls.listeners[clsname] += [func]
            return func
        return wrapper

    def __new__(metacls, clsname, bases, attrs):
        for listener in metacls.listeners.get(clsname, []):
            clsname, bases, attrs = listener(clsname, bases, attrs)
        return super().__new__(metacls, clsname, bases, attrs)
@EventListener.register('User')
def debug(clsname, bases, attrs):
    print(f'Debug: {clsname=}')
    print(f'Debug: {bases=}')
    print(f'Debug: {attrs=}')
    return clsname, bases, attrs


@EventListener.register('User', 'Admin')
def info(clsname, bases, attrs):
    print(f'Info: {clsname=}')
    return clsname, bases, attrs
EventListener.listeners
{'User': [<function debug at 0x1051c0d60>, <function info at 0x1051c0e00>],
 'Admin': [<function info at 0x1051c0e00>]}
class User(metaclass=EventListener):
    pass

Debug: clsname='User'
Debug: bases=()
Debug: attrs={'__module__': '__main__', '__qualname__': 'User', '__firstlineno__': 1, '__static_attributes__': ()}
Info: clsname='User'
class Admin(metaclass=EventListener):
    pass

Info: clsname='Admin'

8.6.19. Use Case - 5

from datetime import datetime, timezone
import logging
from uuid import uuid4


class EventListener(type):
    listeners = {}

    @classmethod
    def register(metacls, *clsnames):
        def decorator(func):
            for clsname in clsnames:
                if clsname not in metacls.listeners:
                    metacls.listeners[clsname] = []
                metacls.listeners[clsname] += [func]
        return decorator

    def __new__(metacls, clsname, bases, attrs):
        listeners = metacls.listeners.get(clsname, [])
        cls = type(clsname, bases, attrs)
        for listener in listeners:
            cls = listener.__call__(cls)
        return cls

Create listener functions and register them for class creation:

@EventListener.register('User', 'Admin')
def add_logger(cls):
    cls._log = logging.getLogger(cls.__name__)
    return cls


@EventListener.register('User')
def add_trace(cls):
    cls._uuid = str(uuid4())
    cls._since = datetime.now(tz=timezone.utc)
    return cls

Now, define classes with EventListener metaclass.

class User(metaclass=EventListener):
    pass

class Admin(metaclass=EventListener):
    pass
pprint(vars(User))
mappingproxy({'__dict__': <attribute '__dict__' of 'User' objects>,
              '__doc__': None,
              '__firstlineno__': 1,
              '__module__': '__main__',
              '__static_attributes__': (),
              '__weakref__': <attribute '__weakref__' of 'User' objects>,
              '_log': <Logger User (WARNING)>,
              '_since': datetime.datetime(2024, 12, 1, 11, 32, 45, 680739, tzinfo=datetime.timezone.utc),
              '_uuid': 'aa237774-d5a7-448f-bb04-034c06419332'})
pprint(vars(Admin))
mappingproxy({'__dict__': <attribute '__dict__' of 'Admin' objects>,
              '__doc__': None,
              '__firstlineno__': 1,
              '__module__': '__main__',
              '__static_attributes__': (),
              '__weakref__': <attribute '__weakref__' of 'Admin' objects>,
              '_log': <Logger Admin (WARNING)>})

8.6.20. Use Case - 6

  • Singleton

class Singleton(type):
    _instances = {}
    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            cls._instances[cls] = super().__call__(*args, **kwargs)
        return cls._instances[cls]


class MyClass(metaclass=Singleton):
    pass
a = MyClass()
b = MyClass()

a is b
True
id(a)
4375248416

id(b)
4375248416

8.6.21. Use Case - 7

  • Final

class Final(type):
    def __new__(metacls, classname, base, attrs):
        for cls in base:
            if isinstance(cls, Final):
                raise TypeError(f'{cls.__name__} is final and cannot inherit from it')
        return type.__new__(metacls, classname, base, attrs)


class MyClass(metaclass=Final):
    pass

class SomeOtherClass(MyClass):
   pass
Traceback (most recent call last):
TypeError: MyClass is final and cannot inherit from it

8.6.22. Use Case - 8

  • Django

Access static fields of a class, before creating instance:


... from django.db import models
...
... # class Model(metaclass=...)
... #     ...
...
...
... class User(models.Model):
...     firstname = models.CharField(max_length=255)
...     lastname = models.CharField(max_length=255)

8.6.23. References

8.6.24. Assignments