Search code examples
pythonpython-typing

How to hint argument to a function as dictionary with parent class in Python


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.


Solution

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