pytype icon indicating copy to clipboard operation
pytype copied to clipboard

pytype reports incorrect [bad-return-type] and crashes on overloads with int literals

Open nik-sm opened this issue 4 years ago • 5 comments

Consider a function whose return type depends on the input.


Defining the function without annotating the return type results in no errors:

def foo(x): 
    if x == 1:
        return ("a", "b")
    else:
        return ("c", "d", "e")


res1, res2 = foo(1)
res3, res4, res5 = foo(5)
$ pytype variable_return.py 
Computing dependencies
Analyzing 1 sources with 0 local dependencies
ninja: Entering directory `.pytype'
[1/1] check variable_return
Leaving directory '.pytype'
Success: no errors found

However, annotating the return type causes pytype to complain about both possible returns:

from typing import Union, Tuple

def foo(x) -> Union[Tuple[str, str], Tuple[str, str, str]]:
    if x == 1:
        return ("a", "b")
    else:
        return ("c", "d", "e")

res1, res2 = foo(1)
res3, res4, res5 = foo(5)
$ pytype variable_return.py
Computing dependencies
Analyzing 1 sources with 0 local dependencies
ninja: Entering directory `.pytype'
[1/1] check variable_return
FAILED: .../variable_return.pyi 
.../python3 -m pytype.single --imports_info .../.pytype/imports/variable_return.imports --module-name variable_return -V 3.8 -o .../.pytype/pyi/variable_return.pyi --analyze-annotated --nofail --quick .../variable_return.py
File ".../variable_return.py", line 11, in <module>: Cannot unpack 3 values into 2 variables [bad-unpacking]
File ".../variable_return.py", line 12, in <module>: Cannot unpack 2 values into 3 variables [bad-unpacking]

For more details, see https://google.github.io/pytype/errors.html#bad-unpacking
ninja: build stopped: subcommand failed.
Leaving directory '.pytype'

Not sure if this is the intended behavior; I could not find it mentioned in the docs anywhere, and it is valid python. Admittedly defining a function with inconsistent return type is probably not a good idea in most cases, but I think sometimes it is the lesser of two evils...

nik-sm avatar Sep 03 '21 19:09 nik-sm

pytype --version         
2021.08.24

nik-sm avatar Sep 03 '21 19:09 nik-sm

Hmm, the correct way to express something like this should be to use overload and Literal:

from typing import Tuple, overload
from typing_extensions import Literal

@overload
def foo(x: Literal[1]) -> Tuple[str, str]: ...

@overload
def foo(x: int) -> Tuple[str, str, str]: ...

def foo(x):
  if x == 1:
    return ("a", "b")
  else:
    return ("a", "b", "c")

However, pytype still reports a [bad-return-type] in this code, and it crashes (!) when I try to run inference. We clearly have a bug somewhere.

rchen152 avatar Sep 04 '21 02:09 rchen152

In the toy example, the "special case" for the function's return type was a literal and you've provided a nice way to state this explicitly. In the actual use case where I encountered this, however, did not have an equality constraint - the behavior was more like:

...
if value > 10:
    return "a"
else:
    return "b", "c"

I think that expressing this as a type signature would require dependent types (but this is not my area of expertise). Is it possible to annotate this situation explicitly using @overloads as you've done above? If it's not possible, perhaps the only type information that can be given would be the simple sort of def fn() -> Union[..., ...] that I wrote initially, but would be interested to learn if there's a better way.

nik-sm avatar Sep 07 '21 14:09 nik-sm

Unfortunately, I don't think that's possible. I believe the best you can do is use a Union like you were doing before and use typing.cast to cast the return value to the right type, or use Any and give up on type-checking the return value.

rchen152 avatar Sep 07 '21 18:09 rchen152

I'm experiencing the same issue, and using typing.cast is a decent workaround 👍🏻

pwildenhain avatar Jan 24 '22 18:01 pwildenhain