pythonmultiple-inheritancemypypython-typing

How do I correctly add type-hints to Mixin classes?


Consider the following example. The example is contrived but illustrates the point in a runnable example:

class MultiplicatorMixin:

    def multiply(self, m: int) -> int:
        return self.value * m


class AdditionMixin:

    def add(self, b: int) -> int:
        return self.value + b


class MyClass(MultiplicatorMixin, AdditionMixin):

    def __init__(self, value: int) -> None:
        self.value = value


instance = MyClass(10)
print(instance.add(2))
print(instance.multiply(2))

When executed this will give the following output:

12
20

The code works.

But running mypy on it, yields the following errors:

example.py:4: error: "MultiplicatorMixin" has no attribute "value"
example.py:10: error: "AdditionMixin" has no attribute "value"

I understand why mypy gives this result. But the mixin classes are never used by themselves. They are always used as additional superclasses.

For context, this is a pattern which has been used in an existing application and I am in the process of adding type-hints. And in this case, the errors are false-positives. I am thinking about rewriting the part using the mixins as I don't particularly like it and the same could probably be done with reorganising the class hierarchy.

But I still would like to know how something like this could be properly hinted.


Solution

  • In addition to Campi's answer about the mypy's recommendation of typing mixins with Protocol:

    An alternative to typing the methods' selfs is just inheriting the protocol.

    from typing import Protocol
    
    
    class HasValueProtocol(Protocol):
        @property
        def value(self) -> int: ...
    
    
    class MultiplicationMixin(HasValueProtocol):
    
        def multiply(self, m: int) -> int:
            return self.value * m
    
    
    class AdditionMixin(HasValueProtocol):
    
        def add(self, b: int) -> int:
            return self.value + b
    
    
    class MyClass(MultiplicationMixin, AdditionMixin):
    
        def __init__(self, value: int) -> None:
            self.value = value
    

    Additionally, if you are TYPE_CHECKING a Protocol, and given that you cannot forward reference a parent class (i.e. passing the parent class as a string literal), a workaround would be:

    from typing import Protocol, TYPE_CHECKING
    
    
    if TYPE_CHECKING:
        class HasValueProtocol(Protocol):
            @property
            def value(self) -> int: ...
    else:
        class HasValueProtocol: ...
    
    
    class MultiplicationMixin(HasValueProtocol):
        def multiply(self, m: int) -> int:
            return self.value * m
    
    ...