I have the following Python function and want to type it in a way that mypy understands the return type as a dictionary, with the items' type as the value type and the id type as the key type:
def id_map(items):
return {item.id: item for item in items}
The current approach looks like this:
from typing import Iterable, Protocol, TypeVar
S = TypeVar("S", covariant=True, bound="ClassWithId")
T = TypeVar("T", covariant=True)
class ClassWithId(Protocol[S, T]):
def __new__(cls: type[S], *args: Any, **kwargs: Any) -> "ClassWithId[S, T]":
...
@property
def id(self: S) -> T:
...
def id_map(items: Iterable[ClassWithId[S, T]]) -> dict[T, S]:
return {item.id: cast(S, item) for item in items}
This currently results in the type builtins.dict[Any, Any]
. I cannot think of a different implementation that would make that work.
I am wondering whether this is possible at all using mypy.
CodePudding user response:
TLDR: Remove the self type variable S
. It is not needed but leads to a type relation that is unlikely to be understood by type checkers.
class ClassWithId(Protocol[T]):
@property
def id(self) -> T:
...
Type variables can be used in two flavours: as inferred variables in a call or as declared variables in a generic type. Use the former for types that are constrained from usage context, and the latter for types that must be consistent across an entire class.
The S
type var is a Self
type; this can always be inferred from usage and does not need to be repeated in the type itself. Otherwise, you construct a recursive type which is likely to confuse current type checkers.
Removing S
from the generic/protocol shows that it is not actually used for any type relation: The T
is fully constrained by the class ClassWithId[T]
. There is no need to refer to S
to define any relation between input and output.
class ClassWithId(Protocol[T]):
@property
def id(self: S) -> T: ...
# ^ defined by generic class, not S
Notice how id_map
also does not depend on S
– knowing either of T
or ClassWithId[T]
is enough to know the other. As a result, one can remove S
completely.
T = TypeVar("T", covariant=True)
class ClassWithId(Protocol[T]):
@property
def id(self) -> T:
...
def id_map(items: Iterable[ClassWithId[T]]) -> dict[T, ClassWithId[T]]:
return {item.id: item for item in items}
If id_map
is supposed to preserve the exact ClassWithId[T]
and not just the protocol, this corresponds to a higher kinded type. In principle, this is expressed by a Type variable with a bound of the protocol.
CWI = TypeVar("CWI", bound=ClassWithId)
def id_map(items: Iterable[CWI[T]]) -> dict[T, CWI[T]]:
return {item.id: item for item in items}
However, MyPy does not supported higher kinded types at the moment.