Jan Rozycki
Jan Rozycki

Reputation: 1753

mypy error - incompatible type despite using 'Union'

Consider following code sample:

from typing import Dict, Union

def count_chars(string) -> Dict[str, Union[str, bool, int]]:
    result = {}  # type: Dict[str, Union[str, bool, int]]

    if isinstance(string, str) is False:
        result["success"] = False
        result["message"] = "Inavlid argument"
    else:
        result["success"] = True
        result["result"] = len(string)
    return result

def get_square(integer: int) -> int:
    return integer * integer

def validate_str(string: str) -> bool:
    check_count = count_chars(string)
    if check_count["success"] is False:
        print(check_count["message"])
        return False
    str_len_square = get_square(check_count["result"])
    return bool(str_len_square > 42)

result = validate_str("Lorem ipsum")

When running mypy against this code, following error is returned:

error: Argument 1 to "get_square" has incompatible type "Union[str, bool, int]"; expected "int"

and I'm not sure how I could avoid this error without using Dict[str, Any] as returned type in the first function or installing 'TypedDict' mypy extension. Is mypy actually 'right', any my code isn't type safe or is this should be considered as mypy bug?

Upvotes: 16

Views: 10013

Answers (1)

Michael0x2a
Michael0x2a

Reputation: 64278

Mypy is correct here -- if the values in your dict can be strs, ints, or bools, then strictly speaking we can't assume check_count["result"] will always evaluate to exactly an int.

You have a few ways of resolving this. The first way is to actually just check the type of check_count["result"] to see if it's an int. You can do this using an assert:

assert isinstance(check_count["result"], int)
str_len_square = get_square(check_count["result"])

...or perhaps an if statement:

if isinstance(check_count["result"], int):
    str_len_square = get_square(check_count["result"])
else:
    # Throw some kind of exception here?

Mypy understands type checks of this form in asserts and if statements (to a limited extent).

However, it can get tedious scattering these checks throughout your code. So, it might be best to actually just give up on using dicts and switch to using classes.

That is, define a class:

class Result:
    def __init__(self, success: bool, message: str) -> None:
        self.success = success
        self.message = message

...and return an instance of that instead.

This is slightly more inconvenient in that if your goal is to ultimately return/manipulate json, you now need to write code to convert this class from/to json, but it does let you avoid type-related errors.

Defining a custom class can get slightly tedious, so you can try using the NamedTuple type instead:

from typing import NamedTuple
Result = NamedTuple('Result', [('success', bool), ('message', str)])
# Use Result as a regular class

You still need to write the tuple -> json code, and iirc namedtuples (both the regular version from the collections module and this typed variant) are less performant then classes, but perhaps that doesn't matter for your use case.

Upvotes: 19

Related Questions