-
Notifications
You must be signed in to change notification settings - Fork 37
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
Check that B.__radd__
is a subtype of A.__add__
for B <: A
#1034
Comments
same issue as #1018? |
no, this has to do with that special >>> from typing import Self, override
...
>>> class Float64(float):
... @override
... def __add__(self, x: float, /) -> Self:
... return type(self)(x)
...
... @override
... def __radd__(self, x: float, /) -> Self:
... return self.__add__(x)
...
>>> type(1.0 + Float64(2.2))
<class '__main__.Float64'> but from typing import Self, override
class Float64(float):
@override
def __add__(self, x: float, /) -> Self:
return type(self)(x)
@override
def __radd__(self, x: float, /) -> Self:
return self.__add__(x)
reveal_type(1.0 + Float64(2.0)) # pyright: float and it's the same thing with the other binary arithmetic ops |
i see. i think it's basically the same issue though but with different dunders. will leave this issue open since #1018 is for the in-place operators |
This one has nothing to do with variance or mutability though 🤔 |
i think the underlying issue in both of these cases is that when a class defines a dunder (eg. |
again, this is also related to #1025, within the family of "the subtype defines something that the super type doesn't know about, but clobbers some functionality that exists on the super type" |
Usually,
However, if, and only if,
This is described in https://docs.python.org/3/library/numbers.html#implementing-the-arithmetic-operations (specifically, step 5.), where they also explain why this is type-safe. So it's not a type-safety thing, it's a python-data-model thing. |
>>> class A:
... def __add__(self, x, /):
... print(f"A.__add__({x!r})")
... def __radd__(self, x, /):
... print(f"A.__radd__({x!r})")
...
>>> class B(A):
... def __radd__(self, x, /):
... print(f"B.__radd__({x!r})")
...
>>> A() + A()
A.__add__(<__main__.A object at 0x762e6ecb6fd0>)
>>> A() + B()
B.__radd__(<__main__.A object at 0x762e6ecb6c10>) while usually >>> class X:
... def __add__(self, x, /):
... print(f"X.__add__({x!r})")
... def __radd__(self, x, /):
... print(f"X.__radd__({x!r})")
...
>>> A() + X()
A.__add__(<__main__.X object at 0x762e6eab52b0>) |
And surprisingly, mypy already does this correctly. |
The ideal solution to this, imho:
However, this requires HKTs + intersection types + proper function types: @overload # RHS is a subtype of LHS and defines __radd__ that can consume LHS.
def add[L, R: L & SupportsRAdd[L]](left: L, right: R) -> ReturnType[R.__radd__, L]: ...
@overload # LHS supports __add__.
def add[R, L: SupportsAdd[R]](left: L, right: R) -> ReturnType[L.__add__, R]: ... |
Yes! That's how should be! I actually wrote optype to make it easier to write annotations like these. But it's too bad that this isn't going to work in practice, because as it turns out, So if in this case, This is why I pointed this out in microsoft/pyright#9663, but it was rejected because it would be "too complex" to fix... So that effectively means that either structural typing shouldn't be used, or that you should never overload your methods. Because if you don't, there will always be a scenario that will be inferred incorrectly. Relavant bpr issue: #989 |
@jorenham I took a look at the comments in microsoft/pyright#9663, I think this problem would also resolve itself if the features I noted above were available, because then instead of def call_f[X, R](obj: CanF[X, R], arg: X) -> R: ... you could do def call_f[X, C: CanF[X, Any]](obj: C, arg: X) -> ReturnType[C.f, X]: ... So then |
Yea, it would indeed be a great alternative, @randolf-scholz. The declarative style is really nice, and doesn't require defining a huge amount of |
i haven't seen any discussion here regarding the consequences of from __future__ import annotations
from typing import reveal_type
class A:
def __add__(self, other: A) -> A:
return A()
class B(A):
def __radd__(self, other: A) -> B:
return NotImplemented
reveal_type(A() + B()) # runtime: A, mypy: B, pyright: A maybe if the signature was |
oof, don't get me started on |
|
|
This comment has been minimized.
This comment has been minimized.
not true, if
this isn't correct, it will be |
>>> from __future__ import annotations
>>> from typing import Literal
>>> class A:
... def __add__(self, other: object) -> A:
... return A()
...
... class B(A):
... def __radd__(self, other: object) -> Literal["anything else"]:
... return "anything else"
...
... class C(A):
... def __radd__(self, other: object) -> A:
... return A()
...
>>> A() + B()
'anything else'
>>> C() + B()
<__main__.A object at 0x7bf636ef0050> |
this is invalid, it's not a subtype of what i mean is, if |
My point is that this actually is valid, at least if we assume conventional LSP rules. So currently, pyright and mypy don't flag this as invalid. |
mypy does flag it as invalid and if we consider |
I'm not sure I follow. Are you suggesting that the |
Yea, I think that makes sense. |
https://mypy-play.net/?mypy=latest&python=3.12&gist=8a93cbf631e9506c446617d459ce3159 although i don't really understand why this doesn't error: |
B.__radd__
is a subtype of A.__add__
for B <: A
, and support __radd__
priority.B.__radd__
is a subtype of A.__radd__
/A.__add__
for B <: A
B.__radd__
is a subtype of A.__radd__
/A.__add__
for B <: A
B.__radd__
is a subtype of A.__add__
for B <: A
Rejected upstream: microsoft/pyright#9684
we shouldn't support
__radd__
priority like mypy, because it's not safe, we can't ensure that RHS is a subtype of LHSThe text was updated successfully, but these errors were encountered: