Main class typing in Python mixins

Viewed 946

I have a class split into mixins:

class MyObject(MyObjectFilesMixin, MyObjectProcessingMixin, ...):
    def __init__(self, value):
        self.value = self.preprocess(value)

A mixin looks like this:

class MyObjectFilesMixin:
    def load_from_file(cls, filename):
        return ...

Now I'd like to add typing to the class and mixins:

class MyObjectFilesMixin:
    def load_from_file(cls, filename: str) -> MyObject:
        return ...


class MyObjectProcessingMixin:
    def preprocess(self: MyObject, value: bytes):
        return value  # logic is omitted

    def append(self: MyObject, other: MyObject):
        self.value += other.value

But it leads to cyclic links. Of course I can create some MyObjectBase (following dependency inversion principle), so that MyObject will also inherit this class, and mixins will use it as argument/return type, but this will lead to wrong types anyway. Is it possible to fix??

I miss so much header+source files from C++

2 Answers

In the case of using mixin inheritance, protocols and structural subtyping and TYPE_CHECKING can help to successfully type classes by avoiding circular imports, it might look like this:

# my_object.py
from mixins import ProcessingMixin


class MyObject(ProcessingMixin):
    def __init__(self, value):
        self.value = self.preprocess(value)

    def some_process(self, value) -> bytes:
        ...
# mixins.py
from typing import Protocol, TYPE_CHECKING

if TYPE_CHECKING:
    from my_object import MyObject

class ValueObjectProto(Protocol):
    """Protocol for value processing methods."""
    value: bytes

    def preprocess(self, value: bytes) -> bytes: ...
    def some_process(self, value) -> bytes: ...


class MyObjectFilesMixin:
    def load_from_file(cls, filename: str) -> MyObject:
        return cast(Type[MyObject], cls)(1)

class ProcessingMixin:
    def preprocess(self: ValueObjectProto, value: bytes) -> bytes:
        value = self.some_process(value)
        return value

    def append(self: ValueObjectProto, other: ValueObjectProto) -> None:
        self.value += other.value

I found how to do the trick without protocols or other unnecessary code, DRY! My solution is simple:

from typing import TYPE_CHECKING, Type
if TYPE_CHECKING:
    from my_object import MyObject
MO = Type('MyObject')


class MyObjectFilesMixin:
    def load_from_file(cls: MO, filename: str) -> 'MyObject':
        # Now I'm able to access all the methods of MyObject by cls
        # with correct IDE support and MyPy checking
        return ...


class MyObjectProcessingMixin:
    def preprocess(self: 'MyObject', value: bytes):
        return value  # logic is omitted

    def append(self: 'MyObject', other: 'MyObject'):
        self.value += other.value

However, I get another warning, probably because MyPy doesn't expect a child to be used as a type in parent:

Mypy: The erased type of self "Type[... MyObject]" is not a supertype of its class "Type[...MyObjectFilesMixin]"

But it's a little cost for the methods and types to be seen and understood correctly by IDE and MyPy!

Related