DEV Community

Cover image for Mastering Metaclasses in Python using real-life scenarios
Muhammad Ihsan
Muhammad Ihsan

Posted on • Updated on

Mastering Metaclasses in Python using real-life scenarios

Metaclasses in Python offer a powerful way to shape how classes are defined, providing developers with the means to enforce coding standards, limiting the number of methods, not allowing public methods, deprecating old methods, and even applying design patterns. In this article, we'll delve into the metaclasses, exploring real-life scenarios where they can be applied for advanced Python coding.

Understanding Metaclasses

Before we explore real-world applications, let's grasp the basics of metaclasses. In Python, metaclasses act as classes for classes, defining how classes are constructed.

Below is a simple MetaClass inherited from the built-in class type:

class MetaClass(type):
    def __new__(cls, name, bases, dct):
        """
        The __new__ method is called when a new class is created (not when an instance is created). It takes four arguments:

        - cls: The metaclass itself.
        - name: The name of the class being created.
        - bases: A tuple of the base/parent classes of the class being created.
        - dct: A dictionary containing all the attributes and methods of the class.

        The purpose of __new__ is to customize the creation of the class object. You can modify the
        attributes or alter the class structure before it is created.
        """
        # Custom logic for creating the class object, modifying attributes, or altering the structure
        return super().__new__(cls, name, bases, dct)

    def __call__(cls, *args, **kwargs):
        """
        The __call__ method is called when an instance of the class is created. It takes three arguments:

        - cls: The class itself.
        - args: The positional arguments passed to the class constructor.
        - kwargs: The keyword arguments passed to the class constructor.

        The purpose of __call__ is to customize the creation or initialization of instances. You can
        perform additional logic before or after instance creation.
        """
        # Custom logic for creating or initializing instances
        return super().__call__(cls, *args, **kwargs)
Enter fullscreen mode Exit fullscreen mode

Real-Life Scenarios

The following are some of the real-life scenarios you need to understand the concept of metaclasses.

1. Enforcing Coding Standards

1.1 Naming Convention Meta Class

The following NamingConventionMeta class ensures the use of proper naming conventions:

class NamingConventionMeta(type):
    def __new__(cls, name, bases, dct):
        first_letter = name[0]
        if not first_letter.isupper():
            raise NameError("Class names must start with an uppercase letter.")
        return super().__new__(cls, name, bases, dct)
Enter fullscreen mode Exit fullscreen mode

Now, let’s try to create new classes, and you’ll see bad_className will raise NameError.

class GoodClassName(metaclass=NamingConventionMeta):
    pass

# will raise NameError
class bad_className(metaclass=NamingConventionMeta):
    pass
Enter fullscreen mode Exit fullscreen mode

1.2 Doc String Meta Class

The DocstringMeta class enforces the presence of docstrings for all methods:

class DocstringMeta(type):
    def __new__(cls, name, bases, dct):
        for attr_name, attr_value in dct.items():
            if callable(attr_value) and not attr_value.__doc__:
                raise TypeError(f"Method '{attr_name}' must have a docstring.")
        return super().__new__(cls, name, bases, dct)
Enter fullscreen mode Exit fullscreen mode

Now, let’s try to create a new class, and you’ll see bad_method will raise TypeError.

class ExampleClass(metaclass=DocstringMeta):

    def good_method(self):
        """ It contains docstring """
        pass

    # will raise TypeError that docstring is missing
    def bad_method(self):
        pass
Enter fullscreen mode Exit fullscreen mode

1.3 Standard Meta Class

By combining multiple metaclasses, we create a StandardClass that enforces various coding standards:

class StandardClass(CamelCase, DocstringMeta):  # Inherited from various Meta classes
    pass
Enter fullscreen mode Exit fullscreen mode

Creating a class with this standard:

class GoodClass(metaclass=StandardClass):
    def good_method(self):
        """ It contains docstring """
        pass
Enter fullscreen mode Exit fullscreen mode

2. Limiting the Number of Methods

The following MethodCountMeta class will allow a maximum of 2 methods. You can also set a different value and set a minimum limit if needed.

class MethodCountMeta(type):
    max_method_count = 2

    def __new__(cls, name, bases, dct):
        method_count = sum(callable(attr_value) for attr_value in dct.values())
        if method_count > cls.max_method_count:
            raise ValueError(f"Class '{name}' exceeds the maximum allowed method count.")
        return super().__new__(cls, name, bases, dct)

class ExampleClass(metaclass=MethodCountMeta):
    def method1(self):
        pass

    def method2(self):
        pass

    # Raises a ValueError since it exceeds the limit
    def method3(self):
        pass
Enter fullscreen mode Exit fullscreen mode

3. Deprecating Methods

The DeprecationMeta metaclass introduces a mechanism to deprecate methods, issuing a warning and providing an alternative.

class DeprecationMeta(type):
    def __new__(cls, name, bases, dct):
        deprecated_methods = {'old_method': 'Use new_method instead'}
        for deprecated_method, message in deprecated_methods.items():
            if deprecated_method in dct and callable(dct[deprecated_method]):
                dct[deprecated_method] = cls._deprecate_method(dct[deprecated_method], message)
        return super().__new__(cls, name, bases, dct)

    @staticmethod
    def _deprecate_method(func, message):
        def wrapper(*args, **kwargs):
            import warnings
            warnings.warn(f"DeprecationWarning: {message}", DeprecationWarning, stacklevel=2)
            return func(*args, **kwargs)
        return wrapper
Enter fullscreen mode Exit fullscreen mode

Now, if you call new_method, it’ll work fine, but calling old_method will raise DeprecationWarning

class Example(metaclass=DeprecationMeta):

    def old_method(self):
        pass

    def new_method(self):
        pass

instance = Example()
instance.new_method()

# will raise DeprecationWarning to use new_method instead
instance.old_method()
Enter fullscreen mode Exit fullscreen mode

4. Applying the Singleton Design Pattern

The Singleton pattern is a design pattern that restricts the instantiation of a class to a single instance and provides a global point of access to that instance. In Python, one way to implement the Singleton pattern is by using a metaclass.

Here's an example of a simple Singleton implementation using a metaclass:

class SingletonMeta(type):
    _instances = {}

    def __call__(cls, *args, **kwargs):
        # If an instance of this class doesn't exist, create one and store it
        if cls not in cls._instances:
            instance = super().__call__(*args, **kwargs)
            cls._instances[cls] = instance
        # Return the existing instance
        return cls._instances[cls]

class SingletonClass(metaclass=SingletonMeta):
    pass
Enter fullscreen mode Exit fullscreen mode

Now, create multiple instances of the class, and check if they are the same instance

instance1 = SingletonClass()
instance2 = SingletonClass()

print(instance1 is instance2)  # Output: True
Enter fullscreen mode Exit fullscreen mode

Conclusion

Mastering metaclasses in Python empowers developers to exert control over class creation, leading to more maintainable, standardized, and robust code. By exploring real-life scenarios, we've demonstrated the versatility of metaclasses in enforcing coding standards, limiting methods, deprecating methods, and applying design patterns.

Incorporating metaclasses into your Python projects allows you to create more maintainable, standardized, and robust code. As you master the art of metaclasses, you gain a deeper understanding of Python's flexibility and extensibility.

Thanks for reading! Feel free to like, comment, and share if you find this article valuable.

You can checkout my other article as well:
Use Asynchronous Programming in Python: Don’t Block entire Thread

Image description

Top comments (16)

Collapse
 
jnussbaum profile image
Johannes Nussbaum

Hi Muhammad,
Thanks for your article, I like it a lot :-)
I think I spotted a mistake: In the very first code example, there is a def __new__() that calls super().__new__(), and then there is a def __call__() that also calls super().__new__(). Is that correct? Shouldn't it call super().__call__()?

Collapse
 
iihsan profile image
Muhammad Ihsan

Yes, you're right, I just corrected it. Thanks for your valuable feedback

Collapse
 
vlc33 profile image
VLC33

It's good to see a working examples for metaclasses, usually explanations about metaclasses started something with "You don't need to use metaclasses, find other way around".

Collapse
 
iihsan profile image
Muhammad Ihsan

Thanks for appreciating my content, it's my first article on dev.to ever, and this really boosted my energy, and now I am trying my best to share more such content. Thanks

Collapse
 
wlagyorgy profile image
wlagyorgy

Great job! It was a pleasure to read. Maybe if you add some explanation for the arguments of the metaclass' methods, even beginners could understand it fully.

Collapse
 
iihsan profile image
Muhammad Ihsan

Thanks for your positive feedback, let me edit it and add more explanations.

Collapse
 
iihsan profile image
Muhammad Ihsan

Hi @wlagyorgy, I just added more explanation for methods and their arguments. Kindly check the article now.
For your reference: I have made changes in the Understanding Metaclasses body.

Collapse
 
wlagyorgy profile image
wlagyorgy

Gorgeous! Thanks!

Collapse
 
ktreharrison profile image
Ken Harrison

Great article!

Collapse
 
iihsan profile image
Muhammad Ihsan

Thanks for your appreciation!

Collapse
 
revisto profile image
Alireza Shabani

Great article, thanks.

Collapse
 
iihsan profile image
Muhammad Ihsan

Thanks for your appreciation.

Collapse
 
maiobarbero profile image
Matteo Barbero

Thanks for your article!

Collapse
 
iihsan profile image
Muhammad Ihsan

Thanks for your appreciation.

Collapse
 
hasii2011 profile image
Humberto A Sanchez II

Nice lucid explanation. I'll keep this in my back pocket

Collapse
 
iihsan profile image
Muhammad Ihsan

Thanks for boosting my energy, a lot much content is on the way...