Search code examples
pythonpython-typingmypy

Why doesn't parameter type "Dict[str, Union[str, int]]" accept value of type "Dict[str, str]" (mypy)


I have a type for a dictionary of variables passed to a template:

VariablesDict = Dict[str, Union[int, float, str, None]]

Basically, any dictionary where the keys are strings and the values are strings, numbers or None. I use this type in several template related functions.

Take this example function:

def render_template(name: str, variables: VariablesDict):
    ...

Calling this function with a dictionary literal works fine:

render_template("foo", {"key": "value"})

However, if I assign the dictionary to a variable first, like this:

variables = {"key": "value"}

render_template("foo", variables)

Mypy gives an error:

Argument 2 to "render_template" has incompatible type "Dict[str, str]"; expected "Dict[str, Union[int, float, str, None]]"

It seems to me that any value of type Dict[str, str] should be safe to pass to a function that expects a parameter of type Dict[str, Union[int, float, str, None]]. Why doesn't that work by default? Is there anything I can do to make this work?


Solution

  • The reason it doesn't work is that Dict is mutable, and a function which accepts a Dict[str, int|float|str|None] could therefore reasonably insert any of those types into its argument. If the argument was actually a Dict[str, str], it now contains values that violate its type. (For more on this, google "covariance/contravariance/invariance" and "Liskov Substitution Principle" -- as a general rule, mutable containers are invariant over their generic type[s].)

    As long as render_template doesn't need to modify the dict you pass to it, an easy fix is to have it take a Mapping (which is an abstract supertype of dict that doesn't imply mutability, and is therefore covariant) instead of a Dict:

    def render_template(name: str, variables: Mapping[str, Union[int, float, str, None]]):
        ...