Multiple inheritance using typing module and mypy

Question:

Consider the following code:

from typing import Union

class A:
  def function_in_a(self) -> str:
    return 'a'

class B:
  def function_in_b(self) -> str:
    return "b"

class C(A, B):
  pass

def call_functions(c: Union[A, B]) -> None:
  print(c.function_in_a())
  print(c.function_in_b())

if __name__=="__main__":
  c = C()
  call_functions(c)

Note that the function call_functions relies on definitions contained in both classes A and B. It expects objects that inherit from both of these classes.

This code will compile when run using python test.py. But mypy --strict test.py throws an error:

test.py:15: note: Revealed type is "Union[test.A, test.B]"
test.py:16: error: Item "B" of "Union[A, B]" has no attribute "function_in_a"
test.py:17: error: Item "A" of "Union[A, B]" has no attribute "function_in_b"
Found 2 errors in 1 file (checked 1 source file)

This makes sense to me. Union means that c can be a subclass of either A or B, but not both. I saw mention of an Intersection type in PEP483 but a quick perusal of the typing module docs showed that this type was never implemented.

How can I get mypy to recognize that parameters of call_functions are objects which inherit from both A and B using type hinting?

Asked By: user32882

||

Answers:

Use typing.Protocol (New in version 3.8.) to define a type that must implement both methods invoked in the function.

from typing import Protocol


class A:
    def function_in_a(self) -> str:
        return 'a'


class B:
    def function_in_b(self) -> str:
        return "b"


class C(A, B):
    pass


class D(B):
    pass


class ProtoAB(Protocol):
    def function_in_a(self) -> str:
        ...

    def function_in_b(self) -> str:
        ...


def call_functions(obj: ProtoAB) -> None:
    print(obj.function_in_a())
    print(obj.function_in_b())


def main() -> None:

    c = C()
    call_functions(c)
    d = D()
    call_functions(d)


if __name__ == "__main__":
    main()
Answered By: Richard Neumann

Another solution is to make A and B Protocol. Protocols can can be used as normal class:

from typing import Protocol
        
class A(Protocol):
    def function_in_a(self) -> str:
        return 'a'
class B(Protocol):
    def function_in_b(self) -> str:
        return "b"
class AB(A, B, Protocol):
    pass
    
def call_functions(c: AB) -> None:
    print(c.function_in_a())
    print(c.function_in_b())

class C(A, B):
    pass
call_functions(C())
Answered By: hussic