Search code examples
pythondictionarypython-typingmypy

mypy doesn't recognize Dictionary of Dictionary


I'm trying to check my type annotations using mypy, but this error keeps ocurring:

Script.py:201: error: Item "Dict[str, Union[float, int]]" of "Union[Dict[str, Union[float, int]], str, float, int, bool]" has no 
attribute "endswith"

My code looks like this:

from typing import Counter, Dict, Iterable, List, NoReturn, Optional, Set, Tuple, Union

TYPE_NUMBER         = Union[float, int]
TYPE_CONFIGURATION  = Dict[str, Union[Dict[str, TYPE_NUMBER], str, float, int, bool]]

def check_configuration(config: TYPE_CONFIGURATION) -> Union[bool, NoReturn]:
    database = 'database'
    assert isinstance(config[database], str)
    assert config[database].endswith('.prdb')

It runs just fine calling python normally. So I know the result of config[database1] is in fact a string. Is the problem is my type alias:

TYPE_CONFIGURATION = Dict[str, Union[Dict[str, TYPE_NUMBER], str, float, int, bool]]

or is it a bug ?

The config is a dict loaded from a JSON file where the only optional parameter is "start". The JSON file looks like this:

{
    "database" : "bla/bla/bla/file.csv",
    "distance" : 800,
    "t"        : false,
    "output"   : "bla/bla/bla/file-out.csv",

    "start"    : {"1": 1343.786, "2": 1356.523}
}

Solution

  • If you know the "start" key in config will always have a value of type Dict[str, Union[int, float]], then one solution could be to use a TypedDict in your type annotation. A TypedDict allows you to specify the expected type of the value associated with each key. In the below example, I've specified total=False, to let mypy know that a dictionary can still be considered a ConfigDict even if it doesn't contain all the fields that were specified in the type definition (you mentioned that "start" was an optional parameter).

    from typing import Dict, Union, TypedDict, NoReturn
        
    TYPE_NUMBER = Union[float, int]
        
    class ConfigDict(TypedDict, total=False):
        database: str
        distance: int
        t: bool
        start: Dict[str, TYPE_NUMBER]
        
    def check_configuration(config: ConfigDict) -> Union[bool, NoReturn]:
        assert isinstance(config['database'], str)
        assert config['database'].endswith('.prdb')
        return True
    

    Subclasses of TypedDict aren't actually types, they're just dictionaries with some extra info attached to them for the benefit of type-checkers. So you can instantiate them like this:

    config = ConfigDict(
        database="bla/bla/bla/file.csv",
        distance=800,
        t=False,
        output="bla/bla/bla/file-out.csv",
        start={"1": 1343.786, "2": 1356.523}
    )
    

    Or like this:

    config: ConfigDict = {
        "database" : "bla/bla/bla/file.csv",
        "distance" : 800,
        "t"        : False,
        "output"   : "bla/bla/bla/file-out.csv",
    
        "start"    : {"1": 1343.786, "2": 1356.523}
    }
    

    — the two syntaxes are identical in the results they produce, and for both of them, type(config) will be dict rather than ConfigDict or TypedDict (isinstance does not work).

    I agree with @MarcelWilson in the comments that Mypy error on dict of dict: Value of type "object" is not indexable is relevant here as to the cause of your problem.