Mastering Metaclasses in Python using real-life scenarios
Tags: python, oop, class, programming
Published: 2024-01-04 04:41:24

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`**:

```python
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)
```

## **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:

```python
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)
```

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

```python
class GoodClassName(metaclass=NamingConventionMeta):
    pass

# will raise NameError
class bad_className(metaclass=NamingConventionMeta):
    pass
```

**1.2 Doc String Meta Class**

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

```python
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)
```

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

```python
class ExampleClass(metaclass=DocstringMeta):

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

    # will raise TypeError that docstring is missing
    def bad_method(self):
        pass
```

**1.3 Standard Meta Class**

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

```python
class StandardClass(CamelCase, DocstringMeta):  # Inherited from various Meta classes
    pass
```

Creating a class with this standard:

```python
class GoodClass(metaclass=StandardClass):
    def good_method(self):
        """ It contains docstring """
        pass
```

### 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.

```python
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
```

### 3. **Deprecating Methods**

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

```python
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
```

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

```python
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()
```

### 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:

```python
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
```

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

```python
instance1 = SingletonClass()
instance2 = SingletonClass()

print(instance1 is instance2)  # Output: True
```

## **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](https://dev.to/iihsan/dont-block-entire-python-thread-use-asynchronous-programming-instead-2pji)

[![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/r3e2estpququ7zq9145g.jpg)](https://dev.to/iihsan/dont-block-entire-python-thread-use-asynchronous-programming-instead-2pji)