Home > Blockchain >  Mypy error Item "None" of "Optional[Dict[str, str]]" has no attribute "get&
Mypy error Item "None" of "Optional[Dict[str, str]]" has no attribute "get&

Time:01-20

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)
  • Related