Python rich comparisons revisited
In my previous blog post Python rich comparison I looked at simplifying the comparison of objects.
Using NotImplemented in boolean context has been deprecated since Python 3.9:
As bool(NotImplemented) is True
this resulted in many wrong implementations, including mine.
So how do you correctly implement rich comparison?
While listening to the episode Talk Python 441: Python = Syntactic Sugar? I learned something new: CPython already does some internal optimizations which saved you from re-implementing this again in Python yourself. Brett Cannon wrote several blog post about Python’s syntactic sugar. There are two very important posts about this:
My most important learnings from them are:
-
if left hand side (LHS) and right hand side (RHS) are of the same type, then CPython will only ever call the comparison method if the LHS, e.g.
1 < 2
will result in onlyint.__lt__(1,2)
being called, not alsoint.__ge__(2, 1)
. -
if one argument is a true sub-type of the other, CPython will ask the sub-type first to compare itself to the super-type, e.g.
1 < my_int(2)
is automatically translated tomy_int.__ge__(2, 1)
. So if you derive your classmy_subclass
from the super-classsuper_class
, make sure your code is able to compare tosuper_class
.
Depending on what you compare, the function should raise TypeError
when incompatible types are compared, but return NotImplemented
if the types are compatible.
For example comparing apples with oranges should return a TypeError
:
>>> 1 == None
False
>>> 1 < None
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: '<' not supported between instances of 'int' and 'NoneType'
>>> "" == 0
False
>>> "" < 0
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: '<' not supported between instances of 'str' and 'int'
>>> object() == True
False
>>> object() < True
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: '<' not supported between instances of 'object' and 'bool'
On the other hand some floating point numbers are integers, but in Python neither is a sub-type of the other:
>>> int(5) == float(5.0)
True
>>> int.__eq__(5, 5.0)
NotImplemented
>>> float.__eq__(5.0, 5)
True
Please note that namedtuple are a sub-class of tuple
, so namedtuple().__eq__
is used when comparing an instance to a tuple
:
>>> from collections import namedtuple
>>> Named = namedtuple("Named", "x y")
>>> Named.__bases__
(<class 'tuple'>,)
>>> plain = (1, 2)
>>> named = Named(*plain)
>>> Named.__eq__(named, plain)
True