I would like to hint a function as a mapping between instances of a class or its children and a value. takes_mapping
below is an example. However, I am getting a static typing error when I use the following:
from collections.abc import Mapping
class Parent:
pass
class Child(Parent):
pass
assert issubclass(Child, Parent)
def takes_mapping(mapping: Mapping[Parent, int]):
return
child = Child()
my_dict: dict[Child, int] = {child: 1}
my_mapping: Mapping[Child, int] = {child: 1}
takes_mapping(my_dict) # typing error...
takes_mapping(my_mapping) # same basic error, involving invariance (see below)
Pyright generates the following error:
Argument of type "dict[Child, int]" cannot be assigned to parameter "mapping" of type "Mapping[Parent, int]" in function "takes_mapping"
"dict[Child, int]" is not assignable to "Mapping[Parent, int]"
Type parameter "_KT@Mapping" is invariant, but "Child" is not the same as "Parent" reportArgumentType
How can I hint the argument to take mapping in such a way that the keys may be an instance of Parent
or any of its children (without typing errors)? In my use case, we may introduce additional children of Parent
and it would be nice we didn't have to couple the hint to the hierarchy, i.e., Union
will not really express what's desired since it depends on the specific unioned types.
The problem is that if the function is typed as taking a Mapping[Parent, ...]
the body of the function is expected to try to access that dict with Parent
keys, and that's likely not going to work if you pass in a dict[Child, ...]
. (It could work depending how you implement __hash__
, and you can make an argument for why mypy should allow it, but you can see why it also might make sense for it to be skittish.)
To fix this I think you want to use a TypeVar
, which allows the type to be narrowed within the bound of Parent
:
from collections.abc import Mapping
from typing import TypeVar
class Parent:
pass
class Child(Parent):
pass
assert issubclass(Child, Parent)
_Parent = TypeVar("_Parent", bound=Parent)
def takes_mapping(mapping: Mapping[_Parent, int]):
# do stuff with the mapping, with some potentially-narrowed type for the mapping keys
return
child = Child()
my_dict: dict[Child, int] = {child: 1}
my_mapping: Mapping[Child, int] = {child: 1}
takes_mapping(my_dict) # ok (_Parent is bound to the Child type)
takes_mapping(my_mapping) # ok (same)