Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

error: Overloaded function signatures 1 and 2 overlap with incompatible return types #4020

Closed
czhang03 opened this issue Sep 27, 2017 · 18 comments

Comments

@czhang03
Copy link

czhang03 commented Sep 27, 2017

I am trying to implement currying in python, it passes Pycharm type checks (which is not as comprehensive as mypy), but my py gives me tons of error like this:

pythonz\basic.py:54: error: Overloaded function signatures 1 and 2 overlap with incompatible return types

Here is a sample of my code

@overload
def curry(f: Callable[[A1], R]) -> PZFunc[A1, R]:
    """curry a function with 1 parameter"""
    ...


@overload
def curry(f: Callable[[A1, A2], R]) -> PZFunc[A1, PZFunc[A2, R]]:
    """curry a function with 2 parameters"""
    ...

def curry(f: Callable) -> PZFunc:
    """To curry a function, only support less than 7 parameters

    :param f: the normal un-curried python function
    :return: A curried PythonZ function
    """
    return PZFunc(lambda x: curry(lambda *args: f(x, *args)))
@czhang03
Copy link
Author

@ilevkivskyi
Copy link
Member

My guess is that mypy considers Callable[[A1], R] and Callable[[A1, A2], R] overlapping types. For example something like def fun(*args): ... matches both.

I am not sure what to do here, maybe we can allow the definition, and give an error at the call site if an argument matches two overloads with incompatible return types. Or maybe we can infer a union of return types if the argument matches multiple overloads.

There is still no 100% agreement on semantics of overloads, see python/typing#253

@ilevkivskyi
Copy link
Member

This appeared again, so I raise the priority. Also python/typing#253 is one of the few issues remaining in the "PEP 484 finalization" typing milestone that approaches soon.

@Daenyth
Copy link

Daenyth commented Oct 20, 2017

In addition to curry/uncurry style functions, I ran into this when I was handling a decorator that was agnostic to the arity of what it worked on by using *args, **kwargs for re-invocation.

@chadrik
Copy link
Contributor

chadrik commented Oct 30, 2017

I ran into this same problem with NamedTuple as well as Callable:

Here are my simple reproductions (using mypy 0.540):

from typing import overload, NamedTuple, Callable

A = NamedTuple('A', [('foo', int)])
B = NamedTuple('B', [('bar', str)])

@overload
def this(arg):
    # type: (A) -> str
    pass

@overload
def this(arg):
    # type: (B) -> int
    pass

def this(arg):
    pass

# -- callable

@overload
def that(arg):
    # type: (str) -> str
    pass

@overload
def that(arg):
    # type: (Callable) -> int
    pass

def that(arg):
    pass

@ilevkivskyi
Copy link
Member

@chadrik I think your particular examples might be related to the fact that if mypy can't determine whether types are overlapping, then it always assumes that they are.

@JelleZijlstra
Copy link
Member

The last example might actually overlap for real with protocols; you could have a str subclass that also defines __call__ and is therefore Callable.

@chadrik
Copy link
Contributor

chadrik commented Oct 31, 2017

The last example might actually overlap for real with protocols; you could have a str subclass that also defines __call__ and is therefore Callable.

The same would be true for any protocol, which effectively means that protocols can't be used with @overload, right? And if mypy takes potential subclasses into consideration when determining overlap, then wouldn't multiple inheritance mean that any two types could overlap? i.e. for any pair of classes A and B there can be a class C that inherits from both, so by the same logic A and B overlap.

Since this is such an easy trap to fall into, it might be worth considering some adjustments to make it easier to avoid. Here are some ideas:

  • only error if overlap is actually present in the declared types. find matches by applying overloads in order of appearance
  • provide a way to declare a type X, but not subclasses of X

I think your particular examples might be related to the fact that if mypy can't determine whether types are overlapping, then it always assumes that they are.

Why can't mypy determine that NamedTuples A and B don't overlap?

@Daenyth
Copy link

Daenyth commented Nov 1, 2017

provide a way to declare a type X, but not subclasses of X - I don't think this is productive. A constraint like that basically means that you expect inheritors of X to break LSP. I think it's reasonable for people who break LSP to see errors.

only error if overlap is actually present in the declared types. - This seems much more sensible to me

@JukkaL
Copy link
Collaborator

JukkaL commented Nov 1, 2017

The way mypy handles overloads is inconsistent. Originally mypy did overlapping checks pretty strictly -- for example, two user-defined classes A and B were considered overlapping, because of potential multiple inheritance. Built-in types were tagged so you could still overload between int and str, for example. This turned out to be too restrictive. Also, as PEP 484 didn't support tagging built-in classes, the rules were relaxed so unrelated classes A and B are no longer considered as overlapping, even though there could still be a common subclass defined somewhere. Some other overlapping rules have been relaxed as well, but we've never generalized the rules to all kinds of types in a consistent way.

provide a way to declare a type X, but not subclasses of X

I don't think that this would be useful, since "type X or any subclass" is so prevalent that this would be very restrictive in practice. For example, all functions in stubs return "type X or any subclass" so about the only the way you can get a guaranteed instance of type X is to call X (or use a type(o) is X check, but that's considered a code smell).

only error if overlap is actually present in the declared types.

I assume that this means that mypy would complain if the actual argument types match multiple overload items with incompatible return types (we'd have to be silent in case of Any argument types to avoid false positives). This might be reasonable thing to do. Alternatively, the type of the call could be inferred to be a union of the return types of all matching overload items.

@ilevkivskyi
Copy link
Member

Originally mypy did overlapping checks pretty strictly

IIUC this is related to the fact that long time ago mypy did some kind of runtime dispatch for overloads.
I will try write a short summary of how I view overloads now, here or in #4159 or in python/typing#253 so that we can move with a PoC implementation as Jukka proposed in #4159 (comment)

@ilevkivskyi
Copy link
Member

All examples in this issue now pass on master.

@miguelmartin75
Copy link

miguelmartin75 commented Dec 3, 2020

I'm running into this issue. I have a simplified example of the issue here: https://gist.github.com/mypy-play/630a35c98137fef0a248078bcb98d193

import typing
from typing import Union, Tuple, List, Literal
import random

class A:
    pass

@typing.overload
def foo(x: Literal[False] = False) -> Union[A, Tuple[A, ...], List[Union[A, Tuple[A, ...]]]]:
    ...

@typing.overload
def foo(x: Literal[True] = True) -> Tuple[Union[A, Tuple[A, ...], List[Union[A, Tuple[A, ...]]]], A]:
    ...

@typing.overload
def foo(x: bool) -> Union[
    Union[A, Tuple[A, ...], List[Union[A, Tuple[A, ...]]]],
    Tuple[Union[A, Tuple[A, ...], List[Union[A, Tuple[A, ...]]]], A]
]:
    ...

def foo(x: bool) -> Union[
    Union[A, Tuple[A, ...], List[Union[A, Tuple[A, ...]]]],
    Tuple[Union[A, Tuple[A, ...], List[Union[A, Tuple[A, ...]]]], A]
]:
    out: Union[A, Tuple[A, ...]] = A() if random.randint(0, 2) else (A(), A())
    if x:
        return ([out], out)
    else:
        return out

I think the ambiguity is due to the first type in the union, second entry (of the @typing.overload'ed function); the Tuple[A, ...]. I've resorted to not overloading the return type and simply performing a # type: ignore in my else branch of the code in order for mypy to be happy. Is there a better solution to this? e.g. via a cast?

@JelleZijlstra
Copy link
Member

Your first two overloads overlap because foo() could match either of them. You should remove the default from one of them.

@miguelmartin75
Copy link

Thanks, didn't realise you should only provide one set of default arguments to one overload. This works great!

@brunonicko
Copy link

What about in the case the default arguments are needed because of the previous default arguments?
In the example below I can't remove the default for mutable because param has a default. Is there a solution for this case?
Thanks and happy holidays!

from typing import Literal, overload


class Attr(object):
    pass


class MutableAttr(Attr):
    pass


@overload
def attr(param=1, mutable=False):
    # type: (int, Literal[False]) -> Attr
    pass

@overload
def attr(param=1, mutable=True):
    # type: (int, Literal[True]) -> MutableAttr
    pass

def attr(param=1, mutable=True):
    # type: (int, bool) -> Attr
    if mutable:
        return MutableAttr()
    else:
        return Attr()

reveal_type(attr(mutable=True))
reveal_type(attr(mutable=False))

Result from mypy (it reveals the correct types, but it fails on incompatible return types):

main.py:15: error: Overloaded function signatures 1 and 2 overlap with incompatible return types
main.py:35: note: Revealed type is 'main.MutableAttr'
main.py:36: note: Revealed type is 'main.Attr'
Found 1 error in 1 file (checked 1 source file)

@JelleZijlstra
Copy link
Member

You can add more overloads to manually cover all the cases, like def attr(*, mutable: bool), def attr(param: int = ...), ... It's not pretty though.

@brunonicko
Copy link

It works! Thanks :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

8 participants