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

Question:

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.

Asked By: exhuma

||

Answers:

I’ve tested it on my machine, hope it will also work for you:

class MultiplicatorMixin:
    value = None # type: int

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


class AdditionMixin:
    value = None # type: int

    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))
Answered By: Sraw

One approach I saw in this question is type hinting the self attribute. Together with Union from the typing package, you are able to use the attributes from a class which is used together with your mixin, while still having correct type hinting for own attributes:

from typing import Union

class AdditionMixin:

    def add(self: Union[MyBaseClass, 'AdditionMixin'], b: int) -> int:
        return self.value + b


class MyBaseClass:

    def __init__(self, value: int):
        self.value = value

Downside is that you have to add the hint to every method, which is kind of cumbersome.

Answered By: soerface

In addition to good answers mentioned above. My use case – mixins to be used in tests.

As proposed by Guido van Rossum himself here:

from typing import *
T = TypeVar('T')

class Base:
    fit: Callable

class Foo(Base):
    def fit(self, arg1: int) -> Optional[str]:
        pass

class Bar(Foo):
    def fit(self, arg1: float) -> str:
        pass    

Thus, when it comes to a mixin, it could look as follows:


class UsefulMixin:

    assertLess: Callable
    assertIn: Callable
    assertIsNotNone: Callable

    def something_useful(self, key, value):
        self.assertIsNotNone(key)
        self.assertLess(key, 10)
        self.assertIn(value, ['Alice', 'in', 'Wonderland']


class AnotherUsefulMixin:

    assertTrue: Callable
    assertFalse: Callable
    assertIsNone: Callable

    def something_else_useful(self, val, foo, bar):
        self.assertTrue(val)
        self.assertFalse(foo)
        self.assertIsNone(bar)  

And our final class would look as follows:

class TestSomething(unittest.TestCase, UsefulMixin, AnotherUsefulMixin):

    def test_something(self):
        self.something_useful(10, 'Alice')
        self.something_else_useful(True, False, None)
Answered By: Artur Barseghyan

For reference, mypy recommends to implement mixins through a Protocol (documentation here).

It works with mypy >= 750.

from typing import Protocol


class HasValueProtocol(Protocol):
    @property
    def value(self) -> int: ...


class MultiplicationMixin:

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


class AdditionMixin:

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


class MyClass(MultiplicationMixin, AdditionMixin):

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

The Protocol base class is provided in the typing_extensions package for Python 2.7 and 3.4-3.7.

Answered By: Campi

Try with:

from typing import Type, TYPE_CHECKING, TypeVar

T = TypeVar('T')


def with_typehint(baseclass: Type[T]) -> Type[T]:
    """
    Useful function to make mixins with baseclass typehint

    ```
    class ReadonlyMixin(with_typehint(BaseAdmin))):
        ...
    ```
    """
    if TYPE_CHECKING:
        return baseclass
    return object

Example tested in Pyright:

class ReadOnlyInlineMixin(with_typehint(BaseModelAdmin)):
    def get_readonly_fields(self,
                            request: WSGIRequest,
                            obj: Optional[Model] = None) -> List[str]:

        if self.readonly_fields is None:
            readonly_fields = []
        else:
            readonly_fields = self.readonly_fields # self get is typed by baseclass

        return self._get_readonly_fields(request, obj) + list(readonly_fields)

    def has_change_permission(self,
                              request: WSGIRequest,
                              obj: Optional[Model] = None) -> bool:
        return (
            request.method in ['GET', 'HEAD']
            and super().has_change_permission(request, obj) # super is typed by baseclass
        )

>>> ReadOnlyAdminMixin.__mro__
(<class 'custom.django.admin.mixins.ReadOnlyAdminMixin'>, <class 'object'>)
Answered By: Leonardo Ramírez

One method that you do not have to write type hint at every method:

import typing


class FooMixin:
    base = typing.Union["Hello", "World"]

    def alpha(self: base):
        self.hello()

    def beta(self: base):
        self.world()


class Base(object):
    pass


class Hello(Base, FooMixin):
    def hello(self):
        print("hello from", self)


class World(Base, FooMixin):
    def world(self):
        print("world from", self)


Hello().alpha()
World().beta()
Answered By: BaiJiFeiLong

My solution: add a value: int without any init to Mixin class:

class MultiplicatorMixin:
    value: int

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


class AdditionMixin:
    value: int

    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))
Answered By: hussic

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

...
Answered By: Nuno André

It’s possible to do something like this when using Protocol and BaseClass in the mixin:

from typing import TYPE_CHECKING, Protocol, cast


class BaseClass:
    def __init__(self) -> None:
        self.name = "base name"


class SizeProtocol(Protocol):
    size: int


class DoubleSizeMixin:
    """
    Add this mixin to classes implementing `SizeProtocol` and inheriting from `BaseClass`
    """

    def get_double_size_with_name(self) -> tuple:
        _self: "DoubleSizeMixinT" = self  # type: ignore
        return (_self.name, _self.size * 2)

    # Another option:
    def get_double_size_with_name_v2(self) -> tuple:
        self = cast("DoubleSizeMixinT", self)  # pylint: disable=self-cls-assignment
        return (self.name, self.size * 2)


if TYPE_CHECKING:
    class DoubleSizeMixinT(SizeProtocol, DoubleSizeMixin, BaseClass):
        pass


class A(BaseClass, SizeProtocol, DoubleSizeMixin):
    def __init__(self) -> None:
        super().__init__()
        self.size = 2


print(A().get_double_size_with_name())
Answered By: Noam Nol