pythonpython-typingmypy

Python generic type on function getting lost somewhere


Getting this typing error:

error: Incompatible types in assignment (expression has type "object", variable has type "A | B")  [assignment]

With this code:

from dataclasses import dataclass
from typing import TypeVar, Mapping, reveal_type


@dataclass
class A:
    foo: str = "a"

@dataclass
class B:
    bar: str = "b"

lookup_table: Mapping[str, type[A] | type[B]] = {
    "a": A,
    "b": B
}

reveal_type(lookup_table)  # note: Revealed type is "typing.Mapping[builtins.str, Union[type[simple.A], type[simple.B]]]"
T = TypeVar("T")

def load(lookup_table: Mapping[str, type[T]], lookup_key:str) -> T:
    con: type[T] = lookup_table[lookup_key]
    instance: T = con()
    return instance

example_a: A | B = load(lookup_table, "a")  # error: Incompatible types in assignment (expression has type "object", variable has type "A | B")
print(example_a)

Edit: Logged a mypy bug here: https://github.com/python/mypy/issues/18265


Solution

  • This is a mypy bug present in 1.13.0 and below (previously reported here and by OP here). pyright and basedmypy both accept the given snippet.

    mypy stores type[A | B] types as a union of types internally (type[A] | type[B]). This is usually convenient, but causes troubles when solving type[T] <: type[A] | type[B] for T, because most types aren't "distributive" (P[A, B] is not equivalent to P[A] | P[B]), and type special case isn't taken into account yet.

    General solver produces meet(A, B) in such case by solving

    type[T] <: type[A]  =>  T <: A  |
                                    |  =>  T <: meet(A, B)
    type[T] <: type[B]  =>  T <: B  |
    

    When A and B have no explicit parent in common, the closest supertype is object. To understand the logic behind that, consider similar equations: x < 2 && x < 3 => x < min(2, 3), and intersection/meet is for types what min operation is for numbers (and union/join is similar to max).

    I submitted a PR to special-case this behaviour, so things may change in a future mypy release.