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}
}
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.