python / mypy

Optional static typing for Python
https://www.mypy-lang.org/
Other
18.19k stars 2.78k forks source link

False positive error: Overloaded function signatures 1 and 2 overlap with incompatible return types #6992

Open huzecong opened 5 years ago

huzecong commented 5 years ago

Are you reporting a bug, or opening a feature request?

Bug.

Please insert below the code you are checking with mypy, or a mock-up repro if the source is private.

from typing import overload, Tuple

@overload
def foo(a: int, c: bool = True) -> int: ...

@overload
def foo(a: int, b: int, *args: int, c: bool = True) -> Tuple[int, ...]: ...

def foo(a, *args, c=True):
    if len(args) == 0:
        return a
    return (a,) + args

if __name__ == '__main__':
    print(foo(1))        # 1
    print(foo(1, 2, 3))  # (1, 2, 3)

What is the actual behavior/output?

mypy reported error: Overloaded function signatures 1 and 2 overlap with incompatible return types.

What is the behavior/output you expect?

The two signatures do not overlap so no error should be raised.

Also, no errors were reported when the argument c was removed.

What are the versions of mypy and Python you are using? Do you see the same issue after installing mypy from Git master?

mypy: The master version (d90eb18). Python: Both 3.7.3 and 3.6.3 are tested and the issue is reproducable.

What are the mypy flags you are using?

No flags were used.

Michael0x2a commented 5 years ago

Alas, this is a legitimate unsafe overlap -- the unsafeness stems from how bools are subtypes of ints in Python.

Basically, we get an unsafe overlap if we try running mypy on following:

# bools are subtypes of ints, so this assignment is safe!
x: int = True

# So we get inconsistent and incompatible results despite that we're using the
# same runtime values:
reveal_type(foo(1, True))  # Revealed type is 'int'
reveal_type(foo(1, x))     # Revealed type is 'Tuple[int, ...]'

Probably the quickest fix would be to just make c a keyword-only argument in your first overload:

@overload
def foo(a: int, *, c: bool = True) -> int: ...

# Note that 'c' is already a keyword-only argument here.
@overload
def foo(a: int, b: int, *args: int, c: bool = True) -> Tuple[int, ...]: ...

def foo(a, *args, c=True):
    if len(args) == 0:
        return a
    return (a,) + args

This will make it so that doing foo(3, True) will no longer select the first overload: you need to do foo(3, c=True) instead.

After this change is made, both calls to foo(...) above will select the second overload and return a Tuple[int, ...]. (This also happens to match what your implementation is actually doing.)

huzecong commented 5 years ago

@Michael0x2a Thank you so much for the explanation! I had no idea that bool was a subtype of int 😲. And yes, making c a keyword-only argument solved the problem.

However, if c is kept as is, and types of all other arguments are changed from int to str, the issue still exists.

@overload
def foo(a: str, c: bool = True) -> str: ...

@overload
def foo(a: str, b: str, *args: str, c: bool = True) -> Tuple[str, ...]: ...

Since bool is not a subtype of str, I believe these two signatures do not overlap. However, mypy still reports the said error.

Michael0x2a commented 5 years ago

Hmm, I think you might be right -- it looks like this is a legitimate bug after all!

Here's a simpler repro:

from typing import overload

@overload
def foo(b: bool = True) -> str: ...

@overload
def foo(a: str, *, b: bool = True) -> int: ...

def foo(*args, **kwargs): pass
bartenra commented 3 years ago

Is there an issue that requests making the error message more explicit about why the function overload has incompatible return types?

Avasam commented 3 weeks ago

@bartenra This? https://github.com/python/mypy/issues/3819