You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
object.__eq__ and other comparisons should have return type Any #3685
@JukkaL commented on Feb 1, 2017
I wonder if the return type of object.__eq__ in typeshed should be object, since the bool return type is not quite correct. The same applies to __lt__ and friends.
Given the implementation, I would suggest the return type should be Union[NoReturn, Any]. object.__eq__ always throws a NotImplementedError (hence the NoReturn) and Any allows sub-classes to override the return type to anything they like.
Please fix the return type of __eq__ and others in typeshed as described in the comment above. __eq__ not returning a boolean is very common, most notably NumPy arrays performing elementwise comparisons.
changed the title [-]> @JukkaL commented on Feb 1, 2017[/-][+]object.__eq__ and other comparisons should have return type Union[NoReturn, Any][/+]on Jan 29, 2020
changed the title [-]object.__eq__ and other comparisons should have return type Union[NoReturn, Any][/-][+]object.__eq__ and other comparisons should have return type Any[/+]on Jan 29, 2020
Please see the thread linked above for details. The statement about raising NotImplementedError was corrected tot returning NotImplemented, so defining Any as the return type should be what we need.
I am honestly not convinced. I would argue that the default assumption is that __eq__ and other binary comparison methods should return a boolean value. If some classes return another type, they can always override the return type, potentially using # type: ignore.
@jonasrauber I very much agree that something needs to be done about __eq__, but there are many nuances around it so I'd recommend discussing with the mypy maintainers before putting in significant time to prep a PR. It seems that there may be some baked-in assumptions and special handling beyond what typeshed dictates.
NoReturn should be a bottom type, a subtype of every type. So Union[NoReturn, Any] is just Any.
Any is not what you want because then you have the undesirable property that fully statically typed code will contain dynamic types (types containing Any). object is the return type you want because (1) it faithfully matches the implementation and (2) it does allow subclasses to correctly override the method.
This is a case where # type: ignore should be used if non-standard behavior is desired.
OK, but I think you should reconsider closing this instead of fixing it.
The non-standard behavior is the annotation in typeshed. It's non-standard because it disagrees with the runtime semantics via implementation of CPython (the only real standard we have for Python). This program has a type error:
deff(x: object) ->int:
returnx==0
But a static type checker that uses the typing in typeshed will not catch it without special handling of the type annotations in class object. I would say instead that the annotations in typeshed should be correct and then that checkers could implement something else if they wanted to.
Are we both talking about the runtime behavior? x == 0 doesn't necessarily invoke object.__eq__. What matters is if the runtime ensures that x == 0 is always bool; and it doesn't. You can decide that the static type of x == 0 is bool but that seems like a bad description of the runtime behavior.
has no type errors assuming that the type of x contains a compatible overload of object.__eq__. However, this is not true for all classes, including the very widely used np.ndarray. Imagine we call f with an instance of such class
A.__eq__ is clearly incompatible with object.__eq__, because str is not a subtype of bool. If we hide that incompatibility via # type: ignore, we will force the type checker to accept an otherwise unsafe f(...) call. Is this WAI?
This is a case where # type: ignore should be used if non-standard behavior is desired.
Rich comparisons have been in Python since 2.1. There's nothing non-standard about it, as @kmillikin said.
I have some snippets here to speed up debugging (I do that with pytest . --pdb) that will require tons of # type: ignore statements if I use this more:
class Falsy:
def __init__(self, reason: str) -> None:
"""Record description."""
self.reason = reason
def __bool__(self) -> typing.Literal[False]:
"""This class is always False."""
return False
def __eq__(self, other: typing.Any) -> bool:
"""Equal to falsy values."""
return not other
def __repr__(self) -> str:
"""Print the reason instead of just False."""
return f'Falsy("{self.reason}")'
Boolean = Falsy | bool
class SomeClass:
...
def __eq__(self, other: Any) -> Boolean: # type: ignore
for attr in self._defining_attributes:
try:
selfattr = getattr(self, attr)
otherattr = getattr(other, attr)
except AttributeError:
return NotImplemented
if selfattr != otherattr:
return Falsy(f"{selfattr} != {otherattr}")
return True
I also don't understand how this is supposed to work with Numpy.
Perhaps we need something like collections.abc.Boolean?
I have encountered the same problem. I am making a query builder for specific needs and it is strange that __eq__ should return bool while other operators shouldn't and there are really popular projects like SQLAlchemy and numpy using rich comparison.
One thing that really stands out about Python compared to other languages, is that it is a great host for embedded domain specific languages (DSLs). This bug means that type checkers that use typeshed for typing == will give false positives for such code, which is too bad.
Put another way: this typing is (gratuitously) unsound with respect to the runtime semantics.
This bug means that type checkers that use typeshed for typing == will give false positives for such code, which is too bad.
As mentioned before, type checkers will happily accept those comparisons if the LSP violation in the overloading classes is # type: ignored.
classFoo:
def__eq__(self, other: object) ->str: # type: ignore[override]return"Hey!"reveal_type(Foo() ==Foo()) # revealed type is `str`
While overloading the operators in common, it is an LSP violation and unsound. Using # type: ignore explicitly to signal intent is the best option. I don't see use changing this. The only completely safe option would be to change the return type to object, which is unreasonable.
@srittau Can you please give us a reference to prove it? According to PEP207 it is not required to return bool for any comparison operator. I do not see there any difference between == and <= operators there
__le__ (and the other comparison operators) is not defined on object in typeshed, which is likely an oversight and should probably be fixed, although that might prove too disruptive. Apart from that I'm not interested in discussing this any further.
Activity
[-]> @JukkaL commented on Feb 1, 2017[/-][+]object.__eq__ and other comparisons should have return type Union[NoReturn, Any][/+][-]object.__eq__ and other comparisons should have return type Union[NoReturn, Any][/-][+]object.__eq__ and other comparisons should have return type Any[/+]jonasrauber commentedon Jan 29, 2020
Please see the thread linked above for details. The statement about raising
NotImplementedErrorwas corrected tot returningNotImplemented, so definingAnyas the return type should be what we need.jonasrauber commentedon Jan 29, 2020
I would be happy to open a PR …
srittau commentedon Jan 29, 2020
I am honestly not convinced. I would argue that the default assumption is that
__eq__and other binary comparison methods should return a boolean value. If some classes return another type, they can always override the return type, potentially using# type: ignore.llchan commentedon Feb 6, 2020
@jonasrauber I very much agree that something needs to be done about
__eq__, but there are many nuances around it so I'd recommend discussing with the mypy maintainers before putting in significant time to prep a PR. It seems that there may be some baked-in assumptions and special handling beyond what typeshed dictates.Related discussions:
python/mypy#2783
python/mypy#5704
python/mypy#6710
srittau commentedon Feb 8, 2022
This is a case where
# type: ignoreshould be used if non-standard behavior is desired.kmillikin commentedon Feb 8, 2022
NoReturnshould be a bottom type, a subtype of every type. SoUnion[NoReturn, Any]is justAny.Anyis not what you want because then you have the undesirable property that fully statically typed code will contain dynamic types (types containingAny).objectis the return type you want because (1) it faithfully matches the implementation and (2) it does allow subclasses to correctly override the method.kmillikin commentedon Feb 8, 2022
OK, but I think you should reconsider closing this instead of fixing it.
The non-standard behavior is the annotation in typeshed. It's non-standard because it disagrees with the runtime semantics via implementation of CPython (the only real standard we have for Python). This program has a type error:
But a static type checker that uses the typing in typeshed will not catch it without special handling of the type annotations in class object. I would say instead that the annotations in typeshed should be correct and then that checkers could implement something else if they wanted to.
JelleZijlstra commentedon Feb 8, 2022
No it doesn't.
object.__eq__returns a bool, and a bool is an int.kmillikin commentedon Feb 8, 2022
Are we both talking about the runtime behavior?
x == 0doesn't necessarily invokeobject.__eq__. What matters is if the runtime ensures thatx == 0is alwaysbool; and it doesn't. You can decide that the static type ofx == 0isboolbut that seems like a bad description of the runtime behavior.superbobry commentedon Feb 9, 2022
I suspect the point was that
has no type errors assuming that the type of
xcontains a compatible overload ofobject.__eq__. However, this is not true for all classes, including the very widely usednp.ndarray. Imagine we callfwith an instance of such classA.__eq__is clearly incompatible withobject.__eq__, becausestris not a subtype ofbool. If we hide that incompatibility via# type: ignore, we will force the type checker to accept an otherwise unsafef(...)call. Is this WAI?joooeey commentedon Nov 18, 2022
Rich comparisons have been in Python since 2.1. There's nothing non-standard about it, as @kmillikin said.
I have some snippets here to speed up debugging (I do that with
pytest . --pdb) that will require tons of# type: ignorestatements if I use this more:I also don't understand how this is supposed to work with Numpy.
Perhaps we need something like
collections.abc.Boolean?dstromberg commentedon May 8, 2023
I'm here because SQLAlchemy also has
__eq__(and others) returning non-bools. I had to convince myself this works with a little test program and some googling, but I'm seeing SQLAlchemy return a BinaryExpression object for use in Object Relational Mapper (ORM) queries:https://docs.sqlalchemy.org/en/20/core/sqlelement.html#sqlalchemy.sql.expression.BinaryExpression
Actually, I believe that Brython uses <= a little interestingly too:
https://brython.info/static_doc/en/faq.html
My first reaction to Brython's was sadness, but in the larger context of SQLAlchemy, Numpy and Brython, I'm getting over it. :)
Here's my test program:
$ cat /tmp/denb
below cmd output started 2023 Mon May 08 01:20:39 PM PDT
So apparently this has worked since 2.1.0.
Tishka17 commentedon Nov 21, 2024
I have encountered the same problem. I am making a query builder for specific needs and it is strange that
__eq__should return bool while other operators shouldn't and there are really popular projects like SQLAlchemy and numpy using rich comparison.Additionally, I want to quote pep-0207
kmillikin commentedon Nov 21, 2024
One thing that really stands out about Python compared to other languages, is that it is a great host for embedded domain specific languages (DSLs). This bug means that type checkers that use typeshed for typing
==will give false positives for such code, which is too bad.Put another way: this typing is (gratuitously) unsound with respect to the runtime semantics.
srittau commentedon Nov 21, 2024
As mentioned before, type checkers will happily accept those comparisons if the LSP violation in the overloading classes is
# type: ignored.While overloading the operators in common, it is an LSP violation and unsound. Using
# type: ignoreexplicitly to signal intent is the best option. I don't see use changing this. The only completely safe option would be to change the return type toobject, which is unreasonable.Tishka17 commentedon Nov 21, 2024
@srittau Can you please give us a reference to prove it? According to PEP207 it is not required to return bool for any comparison operator. I do not see there any difference between
==and<=operators theresrittau commentedon Nov 21, 2024
__le__(and the other comparison operators) is not defined onobjectin typeshed, which is likely an oversight and should probably be fixed, although that might prove too disruptive. Apart from that I'm not interested in discussing this any further.