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

Ordering matters for @overload #1270

Closed
mgeisler opened this issue Mar 4, 2016 · 10 comments · Fixed by #5369
Closed

Ordering matters for @overload #1270

mgeisler opened this issue Mar 4, 2016 · 10 comments · Fixed by #5369

Comments

@mgeisler
Copy link

mgeisler commented Mar 4, 2016

I am trying to model a generic container (a Pandas Series though it shouldn't matter too much for this). The container has a generic "value type" and a generic "index type". When iterating over the container, you get the value type back. So I tried with:

class Series(Generic[VT, IT], Sequence[VT]):

    @overload
    def __getitem__(self, key: slice) -> List[VT]: ...

    @overload
    def __getitem__(self, key: int) -> VT: ...

This gives me an error:

stubs/pandas/__init__.pyi: note: In class "Series":
stubs/pandas/__init__.pyi:186: error: Signature of "__getitem__" incompatible with supertype "Sequence"

That baffled me for a while and on a whim, I tried reordering the two signatures:

class Series(Generic[VT, IT], Sequence[VT]):

    @overload
    def __getitem__(self, key: int) -> VT: ...

    @overload
    def __getitem__(self, key: slice) -> List[VT]: ...

The error is now gone! :-)

When I first got the error, I tried going into typing.py to see exactly what signature Sequence has. That didn't help me since Sequence is defined as

class Sequence(Sized, Iterable[T_co], Container[T_co],
               extra=collections_abc.Sequence):
    pass

I think it could have been helpful if mypy could have written the exact types it was comparing.

This was tested with 0ec0cb4.

@JukkaL
Copy link
Collaborator

JukkaL commented Mar 4, 2016

Yeah, mypy should show the signature of the superclass method.

Also, mypy could perhaps allow you to switch the order of overload variants in case the signatures aren't overlapping, though this is a little questionable as the order could plausibly affect which overload gets picked in a call.

@JukkaL JukkaL added the feature label Mar 4, 2016
@ddfisher
Copy link
Collaborator

ddfisher commented Mar 4, 2016

@JukkaL - what do you mean by "switch the order of overload variants"? Is there some obvious natural ordering that I'm missing?

@JukkaL
Copy link
Collaborator

JukkaL commented Mar 5, 2016

By order of overload variants I mean the textual order of the @overload decorated functions. For example, this is not valid right now:

class A:
    @overload
    def f(self, x: X) -> int: ...
    @overload
    def f(self, x: Y) -> str: ...

class B(A):
    @overload   # note that Y -> str is now the first variant
    def f(self, x: Y) -> str: ...
    @overload
    def f(self, x: X) -> int: ...

Mypy generally picks the first overload variant that matches the argument types when type checking a call. If you change the ordering of variants in a subclass the inferred return type could arbitrarily change for a call depending on whether the receiver type is the base class or the subclass, which is undesirable. However, this is a pretty marginal thing in practice.

@mgeisler
Copy link
Author

mgeisler commented Mar 5, 2016

Interesting... it wasn't even clear to me that there was a defined order. I had somehow imagined that all overloaded methods would have different (non-overlapping) signatures and therefore order wouldn't matter. Since Python itself doesn't have overloaded functions, the semantics of this was not clear from the examples in the tutorial.

@JukkaL
Copy link
Collaborator

JukkaL commented Mar 5, 2016

The reason that there is an order is partially an accident of history, as earlier versions of mypy actually let you use @overload to define functions with multiple bodies, and the decorator would implement runtime dispatch based on argument types. The first matching overload variant was always executed. The runtime dispatch is no longer supported, so mypy could ignore the order of overload variants. However, in practice functions can implement a particular order if they performance isinstance checks, for example. Consider this function:

def f(x):
    if isinstance(x, A):
        return 'x'
    elif isinstance(x, B):
        return 1
    assert False

We could have this overloaded signature in a stub file:

@overload
def f(x: A) -> str: ...
@overload
def f(x: B) -> int: ...

Now if we call f with an instance of a class that is a subclass of both A and B, the first variant would be picked at runtime and the return value would be a string. Thus it makes sense to expect the A -> str variant to be the first one, and give it preference over the B -> int variant.

@gvanrossum
Copy link
Member

gvanrossum commented Mar 6, 2016 via email

@JukkaL
Copy link
Collaborator

JukkaL commented Mar 6, 2016

Actually mypy normally doesn't reject overloads based on potential overlapping that could be introduced via multiple inheritance. I think that the operator method checks might still be picky about this, though.

@pkch pkch mentioned this issue Apr 12, 2017
Michael0x2a added a commit to Michael0x2a/mypy that referenced this issue Jul 17, 2018
This commit is intended to resolve python#1270.

Currently, running mypy on the following program:

    from typing import overload

    class Parent:
        @overload
        def foo(self, x: A) -> A: ...
        @overload
        def foo(self, x: B) -> B: ...

    class Child(Parent):
        @overload
        def foo(self, x: B) -> B: ...
        @overload
        def foo(self, x: A) -> A: ...

...will make mypy report the following error -- it considers the fact
that we've swapped the order of the two variants to be unsafe:

    test.pyi:10: error: Signature of "foo" incompatible with supertype "Parent"

This error message can be confusing for some users who may not be
aware that the order in which your overloads are defined can sometimes
matter/is something mypy relies on.

This commit modifies the error message to the following to try
and make this more clear:

    test.pyi:10: error: Signature of "foo" incompatible with supertype "Parent"
    test.pyi:10: note: Overload variants must be defined in the same order as they are in "Parent"
ilevkivskyi pushed a commit that referenced this issue Aug 2, 2018
This commit is intended to resolve #1270.

Currently, running mypy on the following program:

    from typing import overload

    class Parent:
        @overload
        def foo(self, x: A) -> A: ...
        @overload
        def foo(self, x: B) -> B: ...

    class Child(Parent):
        @overload
        def foo(self, x: B) -> B: ...
        @overload
        def foo(self, x: A) -> A: ...

...will make mypy report the following error -- it considers the fact
that we've swapped the order of the two variants to be unsafe:

    test.pyi:10: error: Signature of "foo" incompatible with supertype "Parent"

This error message can be confusing for some users who may not be
aware that the order in which your overloads are defined can sometimes
matter/is something mypy relies on.

This commit modifies the error message to the following to try
and make this more clear:

    test.pyi:10: error: Signature of "foo" incompatible with supertype "Parent"
    test.pyi:10: note: Overload variants must be defined in the same order as they are in "Parent"
mentalisttraceur added a commit to mentalisttraceur/python-compose-stubs that referenced this issue Aug 17, 2023
Unclear if this order-matters issue is the same as
python/mypy#1270 or a
separate bug, but with the lower-arity overloads
first, the higher-arity overloads were ignored
("too many arguments" errors from MyPy).
@mentalisttraceur
Copy link

mentalisttraceur commented Aug 17, 2023

Not sure if this is considered part of the same issue or not, but I just had a situation where order of overloads made the difference between type checks passing correctly and false Too many arguments for "compose" [call-arg] errors:

Broken:

from typing import Callable, TypeVar, ParamSpec, overload

P = ParamSpec('P')
R1 = TypeVar('R1')
@overload
def compose(f1: Callable[P, R1]) -> Callable[P, R1]:
    ...
R2 = TypeVar('R2')
@overload
def compose(f2: Callable[[R1], R2], f1: Callable[P, R1]) -> Callable[P, R2]:
    ...
R3 = TypeVar('R3')
@overload
def compose(f3: Callable[[R2], R3], f2: Callable[[R1], R2], f1: Callable[P, R1]) -> Callable[P, R3]:
    ...
R4 = TypeVar('R4')
@overload
def compose(f4: Callable[[R3], R4], f3: Callable[[R2], R3], f2: Callable[[R1], R2], f1: Callable[P, R1]) -> Callable[P, R4]:
    ...

Fixed (by changing order)

from typing import Callable, TypeVar, ParamSpec, overload

P = ParamSpec('P')
R1 = TypeVar('R1')
R2 = TypeVar('R2')
R3 = TypeVar('R3')
R4 = TypeVar('R4')

@overload
def compose(f4: Callable[[R3], R4], f3: Callable[[R2], R3], f2: Callable[[R1], R2], f1: Callable[P, R1]) -> Callable[P, R4]:
    ...
@overload
def compose(f3: Callable[[R2], R3], f2: Callable[[R1], R2], f1: Callable[P, R1]) -> Callable[P, R3]:
    ...
@overload
def compose(f2: Callable[[R1], R2], f1: Callable[P, R1]) -> Callable[P, R2]:
    ...
@overload
def compose(f1: Callable[P, R1]) -> Callable[P, R1]:
    ...

@JelleZijlstra
Copy link
Member

We have made some changes to overload resolution since and it is expected that overload order matters. Any related problems should be discussed in a new issue, not in this ancient closed issue.

@mentalisttraceur
Copy link

Turns out the issue in my last comment wasn't even an ordering issue, it was an interleaving issue - eiher order works fine so long as all TypeVar declarations are before all the compose overloads.

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

Successfully merging a pull request may close this issue.

7 participants