Dependent types and polymorphism in Python with mypy

Question:

For the following example, mypy returns an error:

error: Incompatible types in assignment (expression has type “A”,
variable has type “A1”)

from typing import Type

class A:
    pass

class A1(A):
    pass

class A2(A):
    pass

def fun(A_type: Type[A]) -> A:
    if A_type == A1:
        return A1()
    else:
        return A2()

a1: A1 = fun(A1)

What I would ideally like to do is to enforce a dependency in the signature of fun:

def fun(A_type: Type[A]) -> A_type

Is this possible; if not, what is recommended (note: I want this to work for as yet undefined sub-classes of A, so I don’t think I can use the overload decorator)? Is my best option just to use cast?

Asked By: Alex

||

Answers:

Use a TypeVar with a bound on it:

https://mypy.readthedocs.io/en/latest/generics.html#type-variables-with-upper-bounds

from typing import Type, TypeVar

class A:
    pass

class A1(A):
    pass

class A2(A):
    pass

T_A = TypeVar('T_A', bound='A')

def fun(A_type: Type[T_A]) -> T_A:
    if A_type == A1:
        r1 = A1()
        assert isinstance(r1, A_type)
        return r1
    else:
        r2 = A2()
        assert isinstance(r2, A_type)
        return r2

a1: A1 = fun(A1)
a2: A2 = fun(A2)
print("winner winner chicken dinner")

typechecks clean and runs without failing either type assert:

C:testpython>mypy polymorph.py
Success: no issues found in 1 source file

C:testpython>python polymorph.py
winner winner chicken dinner

In this example the type T_A is required to be a subclass of A, but it’s a particular type, and the typing of fun requires that it returns the same type it receives as an argument.

Unfortunately the static type checker isn’t quite smart enough to bind the type unless you add the runtime assert in there (there might be some way to do this better with the Generic type but it eludes me).

Answered By: Samwise

I know this is old, but for completeness’ sake I would like to add that you can also use typing.overload, like this:

from typing import Type, overload

class A:
    pass

class A1(A):
    pass

class A2(A):
    pass

@overload
def fun(A_type: Type[A1]) -> A1: ...

@overload
def fun(A_type: Type[A2]) -> A2: ...

def fun(A_type: Type[A]) -> A:
    if A_type == A1:
        return A1()
    else:
        return A2()

a1: A1 = fun(A1)

This precisely describes the typing for the two variants. As both an upside and downside of this approach, you’ll have to explicitly add another overload for every inheriting class you want to support. Contrast with the bounded TypeVar approach, which automatically annotates the function for use with all inheriting classes. So I guess which approach is preferable still depends on the context and your preferences.

As a sidenote, remember that Python does not really support overloading. There can only be one implementation, so only the last def fun(... definition is what is actually executed when you call fun(...) in your code.
The actual implementation (without the @overload) comes last. The @overload decorator itself is only there as a "tag", so the type checker knows it has to take into account these overloads (instead of overriding them with the last implementation like the interpreter does)

Answered By: DeinAlptraum
Categories: questions Tags: , , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.