4

According to the docs (in Python 3.8):

By default, object implements __eq__() by using is, returning NotImplemented in the case of a false comparison: True if x is y else NotImplemented.

And also:

The correspondence between operator symbols and method names is as follows: [...] x==y calls x.__eq__(y)

So I expect

  1. == to be equivalent to __eq__() and
  2. a custom class without an explicitly defined __eq__ to return NotImplemented when using == to compare two different instances of the class. Yet in the following, == comparison returns False, while __eq__() returns NotImplemented:
class Dummy():
    def __init__(self, a):
        self.a = a

d1 = Dummy(3)
d2 = Dummy(3)

d1 == d2 # False
d1.__eq__(d2) # NotImplemented

Why?

wjandrea
  • 28,235
  • 9
  • 60
  • 81
the.real.gruycho
  • 608
  • 3
  • 17

3 Answers3

7

The reason is that if one side of the operation cannot (or will not) provide an answer, the other side is allowed a say in handling this comparison. A common example is float/int comparisons:

>>> 1 == 1.0
True
>>> (1).__eq__(1.0)
NotImplemented
>>> (1.0).__eq__(1)
True

With int and float, neither is a subclass of the other, and an int doesn't have anything to say about whether it's equal to some float or not. The equality comparison gets handled by the float type.

It's easier to understand if you have distinct types for the left and right sides, and add some debug printout in the hooks:

class Left:
    def __eq__(self, other):
        print("trying Left.__eq__")
        return NotImplemented

class Right:
    def __eq__(self, other):
        print("trying Right.__eq__")
        return True

Demo:

>>> d1 = Left()
>>> d2 = Right()
>>> d1.__eq__(d2)
trying Left.__eq__
NotImplemented
>>> d2.__eq__(d1)
trying Right.__eq__
True
>>> d1 == d2  # Left.__eq__ "opts out" of the comparison, Python asks other side
trying Left.__eq__
trying Right.__eq__
True
>>> d2 == d1  # Right.__eq__ returns a result first, Left.__eq__ isn't considered
trying Right.__eq__
True

Left side type(d1).__eq__ opts out by returning NotImplemented, which allows the right hand side a "second chance" at handling the operation. If left side had returned False instead of returning NotImplemented, Python wouldn't attempt the right side at all and the result would of d1 == d2 be False. If both sides return NotImplemented, like in your Dummy example, then the objects are considered unequal unless they are identical (i.e. same instance).

chepner
  • 497,756
  • 71
  • 530
  • 681
wim
  • 338,267
  • 99
  • 616
  • 750
  • Well, not necessarily *both* sides, but the right-hand side gets a *chance* if the left-hand side refuses or is unable to provide an answer. If `Left.__eq__` returns a value, `Right.__eq__` doesn't get consulted. – chepner Mar 10 '23 at 19:51
  • 1
    @chepner It's not as simple as that. If Right is a subclass of Left, then the right side will get tried first. – wim Mar 10 '23 at 19:55
  • But in that case, if `Right.__eq__` returns a value, then `Left.__eq__` will not be consulted. Your initial wording implies that both can be called, and the actual result could be some sort of combination of the two. (Yes, I'm being extremely pedantic here. I did upvote you :) ) – chepner Mar 10 '23 at 19:59
  • @chepner Huh? Right can also return `NotImplemented`, and then Left will be considered second. – wim Mar 10 '23 at 20:02
  • Sure, but in your example it does not. My point is that *both* are not called and the return value of each used to determine what the final result of `d1 == d2` is. Whatever gets called first prevents the other one from being called at all by returning a value other than `NotImplemented`. – chepner Mar 10 '23 at 20:14
  • Hmm, I'm trying to find a better wording for the answer, but it's difficult. It's like, both types are allowed a say (via their class definition), but it doesn't imply that both opinions will be considered in every comparison. Feel free to edit. – wim Mar 10 '23 at 20:17
  • @chepner Yes, that's better, thanks! – wim Mar 10 '23 at 20:21
4

The documentation here is not very clear and definitely needs to be improved.

When a Python "internal" method returns NotImplemented, it usually means "find some other way to do this operation". What Python does when it can't find "some other way ...." is dependent on the operation, and less well documented than it should be. So for example, if x.__lt__(y) returns NotImplemented, Python might try calling y.__gt__(x).

In the case of ==, Python returns False if x.__eq__(y) and y.__eq__(x) both return NotImplemented. This should be documented better.

wjandrea
  • 28,235
  • 9
  • 60
  • 81
Frank Yellin
  • 9,127
  • 1
  • 12
  • 22
  • https://docs.python.org/3/reference/datamodel.html#object.__lt__ – Mad Physicist Mar 10 '23 at 20:10
  • The last sentence is false. `==` can still return `True` if objects are identical. – wim Mar 10 '23 at 20:14
  • It *really* shouldn't, though, in a good definition of `__eq_`. Equality is supposed to be reflexive, so that `x == x` is true no matter what `x` is. – chepner Mar 10 '23 at 20:15
  • @wim If I understand correctly, the sentence could be changed to "... both return `NotImplemented`, and `x is not y`", right? – wjandrea Mar 10 '23 at 21:43
  • @chepner Yeah, there are some weird edge cases though - ask your [nan](https://en.wikipedia.org/wiki/IEEE_754) – wim Mar 10 '23 at 23:08
  • I don't consider `float.__eq__` to be a good definition. But IEEE 754 did what it could to solve real issues with floating-point arithmetic under less-than-mathematically-ideal conditions. :) – chepner Mar 10 '23 at 23:23
  • @wim `nan.__eq__(nan)` won't return NotImplemented, so it won't go to the second step `x is not y`. – iBug Mar 18 '23 at 06:58
2

== calls the right operand's .__eq__() if the left operand's .__eq__() returns NotImplemented and if both sides return NotImplemented, == will return false.

You can see this behavior by changing your class:

class Dummy():
    def __eq__(self, o):
        print(f'{id(self)} eq')
        return NotImplemented

d1 = Dummy()
d2 = Dummy()
print(d1 == d2)
# 2180936917136 eq
# 2180936917200 eq
# False

This is a common behavior for operators where Python test if the left operand have an implementation, if not, Python call the same or reflection (e.g. if x.__lt__(y) is not implemented y.__gt__(x) is called) operator of the right operand.

class Dummy:
    def __eq__(self, o):
        print(f"{id(self)} eq")
        return NotImplemented

    def __lt__(self, o):
        """reflexion of gt"""
        print(f"{id(self)} lt")
        return NotImplemented

    def __gt__(self, o):
        """reflexion of lt"""
        print(f"{id(self)} gt")
        return False

    def __le__(self, o):
        """reflexion of ge"""
        print(f"{id(self)} le")
        return NotImplemented

    def __ge__(self, o):
        """reflexion of le"""
        print(f"{id(self)} ge")
        return False



d1 = Dummy()
d2 = Dummy()

print(d1 == d2)
# 2480053379984 eq
# 2480053380688 eq
# False

print(d1 < d2)
# 2480053379984 lt
# 2480053380688 gt
# False

print(d1 <= d2)
# 2480053379984 le
# 2480053380688 ge
# False

! Exception to the left before right:

If the operands are of different types, and right operand’s type is a direct or indirect subclass of the left operand’s type, the reflected method of the right operand has priority, otherwise the left operand’s method has priority.

the.real.gruycho
  • 608
  • 3
  • 17
Dorian Turba
  • 3,260
  • 3
  • 23
  • 67