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.
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))
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.
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)
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.
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'>)
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()
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))
In addition to Campi’s answer about the mypy’s recommendation of typing mixins with Protocol
:
An alternative to typing the methods’ self
s 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
...
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())
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.
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))
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.
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)
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.
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'>)
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()
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))
In addition to Campi’s answer about the mypy’s recommendation of typing mixins with Protocol
:
An alternative to typing the methods’ self
s 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
...
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())