Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 518
Jan Rozycki Avatar asked May 15 '17 14:05

Jan Rozycki


1 Answers

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.

like image 79
Michael0x2a Avatar answered Nov 01 '22 00:11

Michael0x2a