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

Mypy doesn't seem to use types from @overload-ed __sub__ #11613

Open
mathialo opened this issue Nov 25, 2021 · 6 comments
Open

Mypy doesn't seem to use types from @overload-ed __sub__ #11613

mathialo opened this issue Nov 25, 2021 · 6 comments
Labels
bug mypy got something wrong topic-overloads

Comments

@mathialo
Copy link

Bug Report

The __sub__ method on the Arrow class from the arrow package has several @overloads:

@overload
def __sub__(self, other: Union[timedelta, relativedelta]) -> "Arrow":
    pass  # pragma: no cover

@overload
def __sub__(self, other: Union[dt_datetime, "Arrow"]) -> timedelta:
    pass  # pragma: no cover

def __sub__(self, other: Any) -> Union[timedelta, "Arrow"]:

    if isinstance(other, (timedelta, relativedelta)):
        return self.fromdatetime(self._datetime - other, self._datetime.tzinfo)

    elif isinstance(other, dt_datetime):
        return self._datetime - other

    elif isinstance(other, Arrow):
        return self._datetime - other._datetime

    return NotImplemented

In particular, one Arrow object minus another Arrow object will always yield a timedelta. However, it seems mypy does not see this, and mistakes the type for Arrow.

To Reproduce

I discovered the issue when finding the time diff between two arrow timestamps:

import arrow

a1 = arrow.get()
a2 = arrow.get()

diff = a2 - a1
print(diff.total_seconds())

Which, when run with mypy yields:

$ mypy test.py
test.py:6: error: "int" not callable
Found 1 error in 1 file (checked 1 source file)

Setting the type explicitly reviles that mypy thinks the type is Arrow, and not timedelta:

from datetime import timedelta
import arrow

a1 = arrow.get()
a2 = arrow.get()

diff: timedelta = a2 - a1
print(diff.total_seconds())

Running mypy:

$ mypy test.py
test.py:7: error: Incompatible types in assignment (expression has type "Arrow", variable has type "timedelta")
Found 1 error in 1 file (checked 1 source file)

Expected Behavior

The second @overload on Arrow's __sub__ method specifies that the diff of two Arrow objects is a timedelta. There should be no type error assigning that to a timedelta variable.

Actual Behavior

Mypy mistakenly thinks the diff is Arrow, which creates a false positive.

Your Environment

  • Mypy version used: 0.910
  • Mypy command-line flags: None
  • Mypy configuration options from mypy.ini (and other config files): None
  • Python version used: 3.9.6
  • Operating system and version: Mac OS 11.6 (Big Sur)
@mathialo mathialo added the bug mypy got something wrong label Nov 25, 2021
@sobolevn
Copy link
Member

sobolevn commented Dec 6, 2021

@mathialo looks like this example works for me on master:

from typing import overload

class Arrow:
    @overload
    def __sub__(self, other: int) -> int:
        ...
    @overload
    def __sub__(self, other: str) -> str:
        ...
    def __sub__(self, other: int | str) -> int | str:
        ...

a: Arrow
reveal_type(a - 1)  # N: Revealed type is "builtins.int"
reveal_type(a - 'a')  # N: Revealed type is "builtins.str"

And this one as well:

from typing import overload, Union, Any
from datetime import timedelta, datetime as dt_datetime

class relativedelta:
    pass

class Arrow:
    @overload
    def __sub__(self, other: Union[timedelta, relativedelta]) -> "Arrow":
        pass  # pragma: no cover

    @overload
    def __sub__(self, other: Union[dt_datetime, "Arrow"]) -> timedelta:
        pass  # pragma: no cover

    def __sub__(self, other: Any) -> Union[timedelta, "Arrow"]:
        pass

a: Arrow
t: timedelta
r: relativedelta
d: dt_datetime
reveal_type(a - t)  # N: Revealed type is "ex.Arrow"
reveal_type(a - r)  # N: Revealed type is "ex.Arrow"
reveal_type(a - d)  # N: Revealed type is "datetime.timedelta"
reveal_type(a - a)  # N: Revealed type is "datetime.timedelta"

Can you please verify?

@tbekolay
Copy link

tbekolay commented Apr 5, 2022

Hello, I'm experiencing this issue as well. I installed the master branch and your simple example work correctly there, but it still is incorrect for the Arrow library. I checked, and in the most recent release (0.942) you get the same behavior, the simple example works correctly but it does not for Arrow.

I noticed that if you switch the order of the __sub__ overloads in arrow.py such that the -> timedelta overload occurs first, then the example from @mathialo works correctly. Is the order in which overloads get specified important? Is there something about arrow's __sub__ implementation that makes mypy unable to correctly infer the type?

@JelleZijlstra
Copy link
Member

Could you provide a minimal example that reproduces the issue? Possible explanations I can see:

  • There is an __rsub__ with an Any argument type, does that make a diifference?
  • Perhaps one of the argument types resolves to Any, causing a particular overload to always match.

And yes, overload order does matter; mypy generally picks the first matching overload.

@tbekolay
Copy link

tbekolay commented Apr 5, 2022

In trying to make a minimal example, I believe I figured out the issue. The issue is that relativedelta in Arrow is provided by the third-party dateutil library, which doesn't ship with types. I believe because of this, relativedelta was being interpreted as Any, which means that the first overload is always matched, giving us Arrow no matter what.

The easy fix is the install pip install types-python-dateutil, which gives the typing information resulting in the correct overloading behavior regardless of order. Any downstream project using Arrow can add this to their requirements as well to resolve this issue. I will also open a PR on Arrow to have this done by default.

In any case, this doesn't appear to be a Mypy issue, so this issue is likely not indicative of a bug. However, it could be a nice Mypy feature to surface this kind of issue through a warning. Perhaps a disable-able warning if any argument to an overload is Any and there is a more specific type for that argument later in the list of overloads?

tbekolay added a commit to tbekolay/arrow that referenced this issue Apr 5, 2022
Not having this installed results in confusing errors when
running `mypy` on the subtraction of two `Arrow` instances.
See python/mypy#11613 for details.
@JelleZijlstra
Copy link
Member

Thanks for figuring that out!

I explored a few simpler examples:

from typing import Any, overload, Union

class X:
    @overload
    def __sub__(self, x: Any) -> int:
        pass
    
    @overload
    def __sub__(self, x: int) -> str:
        pass
    
    def __sub__(self, x: object) -> Any:
        raise NotImplementedError

produces main.py:9: error: Overloaded function signature 2 will never be matched: signature 1's parameter type(s) are the same or broader (great!)

But now we change the first overload to Union[Any, str]:

from typing import Any, overload, Union

class X:
    @overload
    def __sub__(self, x: Union[str, Any]) -> int:
        pass
    
    @overload
    def __sub__(self, x: int) -> str:
        pass
    
    def __sub__(self, x: object) -> Any:
        raise NotImplementedError

And we get main.py:5: error: Overloaded function signatures 1 and 2 overlap with incompatible return types. That's true, but I'd rather see the previous error.

Perhaps we should open a new issue focused on improving this behavior.

krisfremen pushed a commit to tbekolay/arrow that referenced this issue Jul 22, 2022
Not having this installed results in confusing errors when
running `mypy` on the subtraction of two `Arrow` instances.
See python/mypy#11613 for details.
@A5rocks
Copy link
Contributor

A5rocks commented Jun 27, 2024

FYI, as of #17392, the second minimal example no longer shows an error. (due the Any overlap changes, I think)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug mypy got something wrong topic-overloads
Projects
None yet
Development

No branches or pull requests

5 participants