Why does mypy have a hard time with assignment to nested dicts?

mypy version 0.910

Consider

d = {
    'a': 'a',
    'b': {
        'c': 1
    }
}

d['b']['d'] = 'b'

Feeding this to mypy results with

error: Unsupported target for indexed assignment ("Collection[str]")

Putting a side that mypy inferred the wrong type for d (it is clearly not a collection of strings), adding a very basic explicit type for d fixes this:

d: dict = {
    ... # same as above
}

Success: no issues found in 1 source file

I find this very peculiar. mypy should definitely be able to infer that d is a dict without d: dict.

Answer

d is not being inferred as a collection of strings. It is being inferred as a dict, but dicts take two type variables, one for the keys and one for the values. If we use reveal_type:

d = {
    'a': 'a',
    'b': {
        'c': 1
    }
}
reveal_type(d)
d['b']['d'] = 'b'

I get:

(py39) jarrivillaga-mbp16-2019:~ jarrivillaga$ mypy --version
mypy 0.910
(py39) jarrivillaga-mbp16-2019:~ jarrivillaga$ mypy scratch.py
scratch.py:7: note: Revealed type is "builtins.dict[builtins.str*, typing.Collection*[builtins.str]]"
scratch.py:8: error: Unsupported target for indexed assignment ("Collection[str]")
Found 1 error in 1 file (checked 1 source file)

So, it is being inferred as: builtins.dict[builtins.str*, typing.Collection*[builtins.str]] which is a dict mapping strings to collections of strings. This is because for your dict, you have used str and dict[str, int] as values, and I can only surmise that typing.Collection[str] is the least broad type that encompasses str and dict[str, str] ha both. I’m not really sure how mypy is supposed to handle the inference of nested dict literals like yours.

Note, annotating with just dict means it is going to use dict[Any, Any], which you probably don’t want. You should try to give a more constrained type, but that depends on how you intend to use d.