Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Python mypy unable to infer type from union return types

Here is the sample code

from typing import Dict, Union, Tuple


def select_range(data: Dict[str, Union[str, int]]) -> Tuple[int, int]:
    if data['start'] and data['end']:
        return data['start'], data['end']
    return 1, 1

select_range({})

Mypy output:

mypy different_return.py
different_return.py:6: error: Incompatible return value type (got 
"Tuple[Union[str, int], Union[str, int]]", expected "Tuple[int, int]")

Even though one of the dictionary values is int, mypy is unable to infer that.

like image 625
Kracekumar Avatar asked Jul 12 '18 10:07

Kracekumar


1 Answers

Even though one of the dictionary values is int, mypy is unable to infer that.

Mypy is correct. Your code has a bug and mypy is correctly flagging it. There is no guarantee in your code that data['start'] and data['end'] are always going to be integers.

Your data signature is Dict[str, Union[str, int]], so the values have the type Union[str, int]. Mypy must assume that it is always correct to pass in {'start': '2018-07-12', 'end': -42}, so the return value must be Tuple[Union[str, int], Union[str, int]]. Your claim that the function returns Tuple[int, int] clashes with this.

It doesn't matter what actually happens at runtime. That's not the point; mypy is a static typechecker, and is designed to help keep your runtime behaviour bug-free. What matters here is that, according to the type hints, it is possible to pass in non-integer values for start and end, so the typechecker can't protect you from future bugs in your code that accidentally set a string value for either of those two keys.

If you are passing around structured data in dictionaries, you will always have to fight mypy over this, as dictionaries are really the wrong structure for this. You really want to use a named tuple or a dataclass here.

I'm using the name FooBar here, but for your specific application I'm sure there will be a better name for the data structure you are passing around:

from typing import NamedTuple

class FooBar(NamedTuple):
    start: int
    end: int
    # other fields, perhaps with defaults and Optionals


def select_range(data: FooBar) -> Tuple[int, int]:
    if data.start and data.end:
        return data.start, data.end
    return 1, 1
like image 109
Martijn Pieters Avatar answered Sep 22 '22 12:09

Martijn Pieters