The below code runs fine -
from typing import List, Dict, Any
def func1(input_list: List[Dict[str, Dict[str, str]]],):
for config_dict in input_list:
table_1 = ".".join(
(
config_dict.get("src").get("db"), # line 6
config_dict.get("src").get("schema"), # line 7
config_dict.get("src").get("table_name"), # line 8
)
)
print(table_1)
if __name__ == "__main__":
func1(
[
{
"src": {
"db": "db",
"schema": "abc",
"table_name": "bcd",
},
"trgt": {
"db": "dd",
"schema": "asd",
"table_name": "fds",
},
}
]
)
But when I run mypy on it, I get the following error
> mypy abc.py
abc.py:6: error: Item "None" of "Optional[Dict[str, str]]" has no attribute "get" [union-attr]
abc.py:7: error: Item "None" of "Optional[Dict[str, str]]" has no attribute "get" [union-attr]
abc.py:8: error: Item "None" of "Optional[Dict[str, str]]" has no attribute "get" [union-attr]
Am I missing something? I want to keep the input_list structure same.
mypy --version
mypy 0.991 (compiled: yes)
CodePudding user response:
The return value of dict.get
has type Optional[Dict[str, str]]
. That means it could return None
at runtime, so you can't unconditionally assume that the return value has a get
method. That's the error mypy
is catching for you.
You can fix this by checking first that the return value is a dict
.
def func1(input_list: List[Dict[str, Dict[str, str]]],):
for config_dict in input_list:
s = config_dict.get("src")
if s is None:
continue
table_1 = ".".join(
(
s.get("db"), # line 6
s.get("schema"), # line 7
s.get("table_name"), # line 8
)
)
print(table_1)
This is a case where mypy
can perform type-narrowing. It assumes that if execution proceeds after the s is None
, then s
is not None
, and therefore it's type can be "narrowed" (changed to a more specific subtype) from Optional[Dict[str,str]]
to Dict[str, str]
.
You can also stop using get
and use __getitem__
instead. Here, a key-lookup failure is indicated by an exception, rather than a particular return value, so type narrowing is also performed. (Following code will only be reached when the exception is not raised.)
def func1(input_list: List[Dict[str, Dict[str, str]]],):
for config_dict in input_list:
s = config_db["src"]
table_1 = ".".join(
(
s.get("db"), # line 6
s.get("schema"), # line 7
s.get("table_name"), # line 8
)
)
print(table_1)
CodePudding user response:
Your function annotation is too broad: it permits lists of dicts whose keys don't match the expectations of the function. Instead of Dict[str, str]
, use typing.TypeDict
to be more specific. You'll still need to check that config_dict.get("src")
returns a TableConfig
, not None
, but you can assume that a TableConfig
value does have the three keys you try to use. (As in my other answer, I'll switch to config_dict["src"]
to let the lookup fail via exception rather than a special return value.)
from typing import List, Dict, Any, TypedDict
class TableConfig(TypedDict):
db: str
schema: str
table_name: str
def func1(input_list: List[Dict[str, TableConfig]],):
for config_dict in input_list:
table_1 = ".".join(
(
config_dict["src"]["db"],
config_dict["src"]["schema"],
config_dict["src"]["table_name"],
)
)
print(table_1)
You might even be more specific, if "src"
and "target"
aren't arbitrary keys, but part of your schema.
class Mover(TypedDict):
src: TableConfig
target: TableConfig
def func1(input_list: List[Mover],):
for config_dict in input_list:
table_1 = ".".join(
(
config_dict["src"]["db"],
config_dict["src"]["schema"],
config_dict["src"]["table_name"],
)
)
print(table_1)