Can someone explain why this code, while valid, makes the mypy static analyser complain in multiple ways:
ranges = dict()
ranges['max'] = 0
ranges['services'] = []
ranges['services'].append('a')
Namely:
error: Incompatible types in assignment (expression has type "List[<nothing>]", target has type "int")
error: "int" has no attribute "append"
If I simply add a type hint to the initial variable of ranges: dict = dict() it works fine.
I am confused about why the static analyser can't work this out by itself, especially as I am using the dict keyword to initialise the dict in the first instance.
Dictionaries are usually used as keyed collections, the most important operation being to look up the value associated with an arbitrary key. Normally in a dictionary every key has the same type, and every value has the same type; if the values are heterogeneous then the expression ranges[key] wouldn't necessarily have a particular type (though you could represent it as a union).
In your code, the static analyser is trying to infer the type of your dictionary. The type it expects is of the form Dict[K, V] where K and V are yet to be determined. The first assignment ranges['max'] = 0 gives information about both unknowns: K seems to be str and V seems to be int. So at this point, ranges is inferred as type Dict[str, int].
The next two lines then give errors, because an empty list can't be used as a value in a Dict[str, int], and values from a Dict[str, int] don't have an append method.
The explicit type annotation ranges: dict = dict() overrules the default behaviour by specifying that this is a heterogeneous dictionary, so that the values don't all have to have the same type. Given that information, the static analyser doesn't assume that because one of the values is an int that they all have to be ints.
As you have not provided any annotations, mypy must try to infer the types. There are some choices it can make:
ranges = dict() implies range: Dict[Any, Any]
This would accept all of your code. It would also accept ranges['services'].keys() or any other operation since it erases all type information.
ranges['max'] = 0 implies range: Dict[str, int]
This is what mypy picks. It rejects any values that are not integers, in specific your list.
ranges['services'] = [] implies range: Dict[str, list]
This only accepts your ranges['services'] field. The ranges['max'] is invalid then, given that it is not list.
ranges['max'] = 0 and ranges['services'] = [] implies range: Dict[str, Union[int, list]]
This accepts assignment of the values. It rejects ranges['services'].append('a') since ranges['services'] might be an int.
Basically, none of these work. With a plain Dict[K, V], your code cannot be well-typed.
You can use typing.TypedDict to explicitly annotate ranges. Since dict is generally not restricted to static key-value pairs, mypy will not infer this by itself.
class Ranges(TypedDict):
max: int
services: List[str]
ranges: Ranges = dict()
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With