Search code examples
pythontypesmultiple-inheritancetype-hintingmypy

Multiple inheritance using typing module and mypy


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?


Solution

  • 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()