0

The following mathematical relationships between comparison relations (=, ≠, <, >, ≤ and ≥) are always valid and therefore implemented by default in Python (except for the 2 union relationships, which seems arbitrary and is the reason of this post):

  • 2 complementary relationships: "= and ≠ are each other’s complement";
  • 6 converse relationships*: "= is the converse of itself", "≠ is the converse of itself", "< and > are each other’s converse", and "≤ and ≥ are each other’s converse";
  • 2 union relationships: "≤ is the union < and =" and "≥ is the union of > and =".

The following relationships between comparison relations are only valid for total orders and therefore not implemented by default in Python (but users can conveniently implement them when they are valid with the class decorator functools.total_ordering provided by the Python standard library):

  • 4 complementary relationships: "< and ≥ are each other’s complement" and "> and ≤ are each other’s complement".

Why is Python only lacking the 2 union relationships above ("≤ is the union < and =" and "≥ is the union of > and =")?

It should provide a default implementation of __le__ in terms of __lt__ and __eq__, and a default implementation of __ge__ in terms of __gt__ and __eq__, like these (but probably in C for performance, like __ne__):

def __le__(self, other):
    result_1 = self.__lt__(other)
    result_2 = self.__eq__(other)
    if result_1 is not NotImplemented and result_2 is not NotImplemented:
        return result_1 or result_2
    return NotImplemented

def __ge__(self, other):
    result_1 = self.__gt__(other)
    result_2 = self.__eq__(other)
    if result_1 is not NotImplemented and result_2 is not NotImplemented:
        return result_1 or result_2
    return NotImplemented

The 2 union relationships are always valid so these default implementations would free users from having to provide them all the time (like here).

Here is the paragraph of the Python documentation which states explicitly that the 2 union relationships are not currently implemented by default (bold emphasis mine):

By default, __ne__() delegates to __eq__() and inverts the result unless it is NotImplemented. There are no other implied relationships among the comparison operators, for example, the truth of (x<y or x==y) does not imply x<=y.


* Converse relationships are implemented in Python through the NotImplemented protocol.

Géry Ogam
  • 6,336
  • 4
  • 38
  • 67
  • 6
    It does go on to say: *"To automatically generate ordering operations from a single root operation, see `functools.total_ordering()`."* Which says: *"While this decorator makes it easy to create well behaved totally ordered types, it does come at the cost of slower execution and more complex stack traces for the derived comparison methods."* — Mathematical principles are one thing, practicality in a programming context another… – deceze Mar 05 '20 at 08:35
  • 2
    @deceze that doesn't really explain why inferring <= is more impractical than inferring !=. – Alex Hall Mar 05 '20 at 08:37
  • 1
    I just updated my comment. *It's slower.* Inverting `__eq__` is virtually free, the other operations aren't. – deceze Mar 05 '20 at 08:38
  • 1
    curious, is performance the only reason for this? – Paritosh Singh Mar 05 '20 at 08:40
  • @deceze The union relationship that I am referring to has nothing to do with [total orders](https://en.wikipedia.org/wiki/Connex_relation) (also called connex orders), it also holds for [partial orders](https://en.wikipedia.org/wiki/Partially_ordered_set#Formal_definition). Total orders are only necessary for the [complement](https://en.wikipedia.org/wiki/Binary_relation#Complement) relationship: < is the complement of ≥, and > is the complement of ≤ _only for total orders_. That is why the Python decorator that you are referring to is called `functools.total_ordering`. – Géry Ogam Mar 05 '20 at 08:40
  • 1
    To derive those relationships you'd need to execute *one or more* of the implemented methods. That may be a lot slower than implementing `__ge__` as a single method. You can explicitly opt-into the slow behaviour with the `total_ordering` decorator if you want to and you're aware of the consequences. – deceze Mar 05 '20 at 08:44
  • 1
    @deceze The fact that `functools.total_ordering` is not the default has nothing to do with performance, it is because the implied relationships are only valid for _total orders_. Otherwise it would have been implemented in C, like `__ne__` whose complementary relationship with `__eq__` is always valid. – Géry Ogam Mar 05 '20 at 08:51
  • 4
    What would constitute a valid reason for you? Say, a situation where ``<=`` is not equal to ``< or ==``? Or a stylistic reason, such as "explicit is better than implicit"? The actual reason is only known to the persons who made that decision. – MisterMiyagi Mar 05 '20 at 08:54
  • @MisterMiyagi I cannot find a reason, that is why I am asking. – Géry Ogam Mar 05 '20 at 08:58
  • 2
    *"Otherwise it would have been implemented in C"* — Well… this is about custom comparison methods on user defined classes! Whose comparison methods are implemented in userland Python code. To implicitly derive relationships, the C code would need to make calls like `return o.__lt__() or o.__eq__()`, calling *potentially slow userland Python code twice*. That's not an issue with `return not o.__eq__()`. – deceze Mar 05 '20 at 08:59
  • 5
    @Maggyero I'm not asking whether you know a reason. I'm asking what *kind* of answer would be acceptable to you. With the exception of the people who actually made the decision, everyone else can only offer an opinion or guess [which is out of scope for SO](https://stackoverflow.com/help/dont-ask). Unless you clarify the question to make it clear what would constitute an *objective* answer (e.g. counter example, stylistic, technical), the question is unanswerable. – MisterMiyagi Mar 05 '20 at 09:01
  • @MisterMiyagi Any answer that makes sense would be acceptable. – Géry Ogam Mar 05 '20 at 09:09
  • 4
    And performance considerations *don't* make sense…? – deceze Mar 05 '20 at 09:13
  • @deceze Saying that delegating to one method (`return not o.__eq__()`) is okay but to two methods (`return o.__lt__() or o.__eq__()`) is not doesn’t really make sense. Especially as nothing prevents you for overloading the method. – Géry Ogam Mar 05 '20 at 09:17
  • 4
    You're not seeing that *inverting a boolean* has virtually no overhead, while *implicitly calling two functions of unknown complexity* may have a ton of overhead?! – deceze Mar 05 '20 at 09:19
  • @deceze Calling `o.__eq__()` has no overhead and is of known complexity? – Géry Ogam Mar 05 '20 at 09:20
  • 5
    Deriving `__ne__` from `__eq__` has no overhead, it is virtually free. Deriving `__le__` from the union of `__lt__` and `__eq__` though doubles the performance cost. Which is something you can explicitly opt-into with the `total_ordering` decorator if you're fine with that behaviour, but is not something Python will implicitly do and then leave you to search for your performance bottlenecks. – deceze Mar 05 '20 at 09:22
  • I think at this point, i'd personally be happy to see a collation of that final comment and your previous remarks as an answer deceze – Paritosh Singh Mar 05 '20 at 09:24
  • @deceze Deriving `__ne__` from `__eq__` has the complexity of calling `__eq__` so I don’t buy that argument. – Géry Ogam Mar 05 '20 at 09:28
  • 1
    If you want to do *any* equality comparison, *a* method will need to be called anyway. The difference between `__eq__()` and `not __eq__()` is essentially non-existent, so can be safely derived. – deceze Mar 05 '20 at 09:36

3 Answers3

8

Why exactly this decision was made only the original author knows, but given these hints from the manual reasons can be inferred:

To automatically generate ordering operations from a single root operation, see functools.total_ordering().

While this decorator makes it easy to create well behaved totally ordered types, it does come at the cost of slower execution and more complex stack traces for the derived comparison methods. If performance benchmarking indicates this is a bottleneck for a given application, implementing all six rich comparison methods instead is likely to provide an easy speed boost.

Pair this with Python's mantra of explicit is better than implicit, the following reasoning should be satisfactory:

Deriving __ne__ from __eq__ is virtually free, it's just the operation not o.__eq__(other), i.e. inverting a boolean.

However, deriving __le__ from the union of __lt__ and __eq__ means that both methods need to be called, which could be a potentially large performance hit if the comparison done is complex enough, especially compared to an optimised single __le__ implementation. Python lets you opt-into this convenience-over-performance explicitly by using the total_ordering decorator, but it won't implicitly inflict it on you.

You could also argue for explicit errors if you attempt to do unimplemented comparisons instead of implicitly derived comparisons which you didn't implement and which may create subtle bugs, depending on what you meant to do with your custom classes. Python won't make any guesses for you here and instead leave it up to you to either explicitly implement the comparisons you want, or to again explicitly opt-into the derived comparisons.

deceze
  • 510,633
  • 85
  • 743
  • 889
  • As I said earlier, the following union relationships are always valid: ≤ is the union of < and =, and ≥ is the union of > and =, contrary to the following complementary relationships which are only valid for _total orders_ (also called connex orders): < is the complement of ≥, and > is the complement of ≤. That is why the complementary relationships are not implemented by default but can be user implemented when they are valid, using the convenient `functools.total_ordering` class decorator of the Python standard library. So it has nothing to do with performance in my opinion. – Géry Ogam Mar 05 '20 at 10:05
  • What about a type like `NaN`, which is incomparable? Even if performance isn't a valid reason in your opinion, I offered an entire second paragraph with additional reasoning for why Python might not want to derive implicit relationships. You could say that Python shouldn't even derive `__ne__` for cases like `NaN`, but if you do have an edge case like `NaN` where `__ne__` isn't the inverse of `__eq__`, it's one thing to have to override *one* implicit derived comparison to cover that edge case, it's another to have to override half a dozen. – deceze Mar 05 '20 at 10:05
  • 1
    Python isn't pure mathematics. However nice and pure the abstract relationships are, as a programming language Python does need to factor in things like performance, obviousness, simplicity, implementation details and practical usage. – deceze Mar 05 '20 at 10:08
  • 1
    If you do too many implicit things, you'll end up with niceties like PHP's `0 == 'foo'` (yes, that's true) or Javascript's `'' == []` (yes, also true). Python's designers opt for virtually *no implicit assumptions*, except for the pretty obvious `__ne__` case which they deemed safe enough and common enough and which isn't too hard to override should you ever get into the edge case where you need to override it. Again, ***many reasons*** contributed to this design decision, and I have listed two in my answer and a few more in the comments. – deceze Mar 05 '20 at 10:15
  • What is the problem with `math.nan`? It is incomparable yes. And? `math.nan == math.nan` is `False`, `math.nan < math.nan` is also `False`, and so is `math.nan <= math.nan`, which can be derived from the previous two. I don’t propose to derive all binary relations from the others, only the ones that are always true and are currently missing in the Python implementation: ≤ is the union of < and =, and ≥ is the union of > and =. That’s all. – Géry Ogam Mar 05 '20 at 10:18
  • 1
    Again, the point is, there are weird edge cases about which it isn't safe to make assumptions, **so Python simply doesn't make any**. You could contrive any number of weird examples, even if they don't make sense, but they could be implemented, and people may want to implement them. It doesn't matter what the mathematical theory says, that's one tiny piece of the big picture. The language designer(s) decided that it is safer to not derive missing comparison methods by default, period. – deceze Mar 05 '20 at 10:22
  • Maybe, but could you provide an example? – Géry Ogam Mar 05 '20 at 10:23
  • I feel I have given enough reasons and examples. It is the way it is. I have illustrated the thinking that may or may not have led the designers to implement it that way. It does not need to make *mathematical sense*. It needs to make practical sense in the context of creating and maintaining a programming language. And if my many examples haven't convinced you yet, I don't know what will. – deceze Mar 05 '20 at 10:29
  • The only example that you provided (`math.nan`) was not valid (or I missed something). So please provide a valid one. All I am asking is to be convinced. – Géry Ogam Mar 05 '20 at 10:35
  • 3
    I can only repeat myself: deriving `__le__` from `__eq__` and `__lt__` may create performance issues, alternatively trying to derive `__le__` from `not __gt__` may fail for types like `NaN`, unimplemented comparisons *not* raising an error is arguably surprising behaviour… **Taken all these various reasons _together_, the Python designer(s) opted to not implement default derived comparisons.** Don't focus on *one* argument, consider them in their totality. – deceze Mar 05 '20 at 10:39
  • With this reasoning they would not have implemented `__ne__` in terms in `__eq__`. That does not make sense. – Géry Ogam Mar 05 '20 at 11:40
  • 1
    Again, *practicality*. Testing for the inverse of equality is basically just a syntax thing. Whether you write `if foo: return a else: return b` or `if not foo: return b else: return a` has no semantic difference and is basically just a decision on how readable your code is. And *usually*, `__ne__` is just `not __eq__`. Requiring everyone to explicitly implement this *very common* operation explicitly when it can be *safely derived 99% of the time* would make Python worse. I feel that's really what's missing here, you not being able to adopt that perspective somehow. – deceze Mar 05 '20 at 12:18
  • 1
    I could use the exact same argument for `__le__` and `__ge__`: they can be safely derived 99% of the time. – Géry Ogam Mar 05 '20 at 12:36
6

Your question is based on a number of incorrect assumptions. You start your question with:

The following mathematical relationships between comparison relations (=, , <, >, and ) are always valid and therefore implemented by default in Python (except for the 2 union relationships, which seems arbitrary and is the reason of this post).

There is no default implementation for < or > either. There is no default __lt__ or __gt__ implementation, so there can't be a default implementation for __le__ or __ge__ either.*

This is covered in the expressions reference documentation under Value Comparisons:

The default behavior for equality comparison (== and !=) is based on the identity of the objects. Hence, equality comparison of instances with the same identity results in equality, and equality comparison of instances with different identities results in inequality. A motivation for this default behavior is the desire that all objects should be reflexive (i.e. x is y implies x == y).

A default order comparison (<, >, <=, and >=) is not provided; an attempt raises TypeError. A motivation for this default behavior is the lack of a similar invariant as for equality.

The behavior of the default equality comparison, that instances with different identities are always unequal, may be in contrast to what types will need that have a sensible definition of object value and value-based equality. Such types will need to customize their comparison behavior, and in fact, a number of built-in types have done that.

The motivation to not provide default behavior is included in the documentation. Note that these comparisons are between the value of each object, which is an abstract concept. From the same documentation section, at the start:

The value of an object is a rather abstract notion in Python: For example, there is no canonical access method for an object’s value. Also, there is no requirement that the value of an object should be constructed in a particular way, e.g. comprised of all its data attributes. Comparison operators implement a particular notion of what the value of an object is. One can think of them as defining the value of an object indirectly, by means of their comparison implementation.

So comparisons are between one notion of the value of an object. But what that notion is exactly, is up to the developer to implement. Python will not assume anything about the value of an object. That includes assuming that there is any ordering inherent in the object values.

The only reason that == is implemented at all, is because when x is y is true, then x and y are the exact same object, and so the value of x and the value of y are the exact same thing and therefore must be equal. Python relies on equality tests in a lot of different places (like testing for containment against a list), so not having a default notion of equality would make a lot of things in Python a lot harder. != is the direct inverse of ==; if == is true when the values of the operands are the same, then != is only true when == is false.

You can't say the same for <, <=, => and > without help from the developer, because they require much more information about how the abstract notion of a value of the object needs to be compared to other similar values. Here, x <= y is not simply the result inverse of x > y, because there isn't any information about the values of x or y, and how that relates to == or != or < or any other value comparison.

You also state:

The 2 union relationships are always valid so these default implementations would free users from having to provide them all the time

The 2 union relationships are not always valid. It may be that the > and < operator implementation is not making comparisons and an implementation is free to return results other than True or False. From the documentation on the __lt__ etc. methods:

However, these methods can return any value, so if the comparison operator is used in a Boolean context (e.g., in the condition of an if statement), Python will call bool() on the value to determine if the result is true or false.

If an implementation decides to give > and < between two objects a different meaning altogether, the developer should not be left with incorrect default implementations of __le__ and __ge__ that assume that the implementation for __lt__ and __gt__ return booleans, and so will call bool() on their return values. This may not be desireable, the developer should be free to overload the meaning of __bool__ too!

The canonical example for this is the Numpy library, which was the primary driver for implementing these rich comparisons hooks. Numpy arrays do not return booleans for comparison operations. Instead, they broadcast the operation between all contained values in the two arrays to produce a new array, so array_a < array_b produces a new array of boolean values for each of the paired values from array_a and array_b. An array is not a boolean value, your default implementation would break as bool(array) raises an exception. While in Numpy's case they also implemented __le__ and __ge__ to broadcast the comparisons, Python can't require all types to provide implementations for these hooks just to disable them when not desired.

You appear to be conflating mathematical relationships with Python's use of some of those relationships. The mathematical relationships apply to certain classes of values (numbers, mostly). They do not apply to other domains, it is up to the implementation of each type to decide whether to honour those mathematical relationships.

Finally, the complementary relationship between < and >=, and between > and <= *only applies to total order binary relationships, as stated in the complement section of the Wikipedia article on binary relation:

For example, = and are each other's complement, as are and , and , and and , and, for total orders, also < and , and > and .

Python can't make the assumption that all type implementations wish to create total order relations between their values.

The standard library set type, for example, does not support total order between sets, set_a < set_b is true when set_a is a subset of a larger set_b. This means there can be a set_c that is a subset of set_b but set_c is not necessarily a subset, or superset of set_a. Set comparisons are also have no connexity, set_a <= set_b and set_b <= set_a can both be false, at the same time, when both sets have elements that are not present in the other.


* Note: the object.__lt__, object.__gt__, object.__le__ and object.__ge__ methods do have a default implementation, but only to return NotImplemented unconditionally. They exist only to simplify the implementation of the <, >, <= and >= operators, which for a [operator] b need to test a.__[hook]__(b) first, then try b.__[converse hook]__(a) if the first returns NotImplemented. If there was no default implementation, then the code would also need to check if the hook methods exist first. Using < or > or <= or >= on objects that do provide their own implementations results in a TypeError, nonetheless. Do not regard these as default implementations, they do not make any value comparisons.

Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
  • Thanks Martijn for your input. "There is no default `__lt__` or `__gt__` implementation, so there can't be a default implementation for `__le__` or `__ge__` either." There is a misunderstanding, I was talking about the implementation of the *relationships* between the comparison methods, not about the implementation of the comparison methods themselves which are indeed just `return NotImplemented` (except for `__ne__`, cf. my answer [here](https://stackoverflow.com/a/50661674/2326961)). – Géry Ogam Sep 28 '20 at 15:25
  • 1
    @Maggyero: it is your claim that the *mathematical relationships between comparison relations* are always valid. That's not the case. The Python implementation makes no such guarantees, because it is up to the implementation of the specific types to honour those relationships. – Martijn Pieters Sep 28 '20 at 15:30
  • 1
    I disagree with the rest of the answer since it contradicts the fact that the 6 converse relationships are implemented: "= is the converse of itself", "≠ is the converse of itself", "< and > are each other’s converse", and "≤ and ≥ are each other’s converse". For instance, if you define `__lt__` on a class `X` and `__gt__` on a class `Y`, and evaluates `x < y`, Python will take advantage of the converse relationship between < and > by calling `x.__lt__(y)` first and if that fails (returns `NotImplemented`) by calling `y.__gt__(x)` (cf. the `lt` implementation that I gave in the link). – Géry Ogam Sep 28 '20 at 15:43
  • @Maggyero: the reason those are implemented is to support delegation to the RHS operand. Nothing more. – Martijn Pieters Sep 28 '20 at 15:47
  • 1
    @Maggyero: and Python can't assume that all value relationships will be total order relations either. `<` is only the complement of `>=` if a total order relation is assumed. – Martijn Pieters Sep 28 '20 at 15:48
  • "< is only the complement of >= if a total order relation is assumed" That is exactly what I said in my post: "The following relationships between comparison relations are only valid for total orders and therefore not implemented by default in Python (but users can conveniently implement them when they are valid with the class decorator `functools.total_ordering` provided by the Python standard library)" But here we are not talking about the 4 *complementary* relationships but about the 2 *union* relationships, which are valid for *any* orders. – Géry Ogam Sep 28 '20 at 15:55
  • @Maggyero: that still assumes an ordered relationship. That's not a given, Python doesn't require types to implement *any* ordering. – Martijn Pieters Sep 28 '20 at 15:58
  • 1
    @Maggyero: `numpy_array_a < numpy_array_b` produces a new array, numpy arrays are not ordered, and instead the comparison operations are *broadcast* across the values contained. – Martijn Pieters Sep 28 '20 at 15:59
  • "that still assumes an ordered relationship." I am talking about defaults only in Boolean context in this post, otherwise yes anything could happen. Even equality `==` would not be defined by default (as the Boolean value of identity). – Géry Ogam Sep 28 '20 at 16:07
  • 1
    @Maggyero: I do explain in my answer why `==` has a default boolean implementation. Equality between two identical objects does not require any ordering assumptions. – Martijn Pieters Sep 28 '20 at 16:09
  • Yes I noticed, but even that is wrong in non-Boolean context. `is` returns a Boolean. – Géry Ogam Sep 28 '20 at 16:10
  • @Maggyero: I'm going to bow out here, the comment thread is too long and I unfortunately have no time to spare for this. I'm not sure what point you were trying to make with the last set of comments. At this point I'm simply regretting even writing an answer here, as your question looks more and more seeking confirmation of your opinion. – Martijn Pieters Sep 28 '20 at 16:12
  • Thanks for your contribution, I have just upvoted your answer as I like the identity implies equality argument (I may even accept it as to me it is the most convincing). But understand that I may disagree when you state things that I have not said (I have never said that the 4 complementary relationships valid only for total orders should be the default) or that are out of context (anything can be defined in the non-Boolean case, but that does not invalidate the defaults in the Boolean case like `==` delegating to `is`, so why would it invalidate the 2 missing union relationships?). – Géry Ogam Sep 28 '20 at 16:39
  • The documentation states: "A motivation for this default behavior is the desire that all objects should be reflexive (i.e. `x is y` implies `x == y`). A default order comparison (`<`, `>`, `<=`, and `>=`) is not provided; an attempt raises `TypeError`. A motivation for this default behavior is the lack of a similar invariant as for equality." The last sentence is incorrect since `<=` and `>=` are also *reflexive* like `==` (contrary to `!=`, `<` and `>`). Indeed, `x is y` implies `x <= y` and `x >= y`. So `<=` and `>=` could also be defined by default as `is` instead of raising a `TypeError`. – Géry Ogam Sep 29 '20 at 09:51
  • We have been talking about the *reflexibility* property of `==` (`x == x` evaluates to `True`), `<=` (`x <= x` *should* evaluate to `True` instead of raising `TypeError`) and `>=` (`x >= x` *should* evaluate to `True` instead of raising `TypeError`). I have just realised that we have also overlooked the *irreflexibility* property of `!=` (`x != x` evaluates to `False`), `<` (`x < x` *should* evaluate to `False` instead of raising `TypeError`) and `>` (`x > x` *should* evaluate to `False` instead of raising `TypeError`). Thus not only `<=` and `>=` but also `<` and `>` don’t behave as expected. – Géry Ogam Oct 03 '20 at 12:41
  • @Maggyero: that's an arbitrary choice however. Raising `TypeError` is just as valid because there is no ordering for objects that don't explicitly set one. I **prefer** the `TypeError` because if I tried to, say, apply sorting or comparisons to objects that don't support a total order, I *probably* made a bug somewhere. – Martijn Pieters Oct 03 '20 at 15:24
  • Interesting. But the same argument can be applied to `==` and `!=`: there is no equality/inequality for objects that do not explicitly set one, so they should raise `TypeError`. The *only* reason why Python does not raise `TypeError` for them is because when their operands are *identical*, there is an obvious answer: `True` for `==` and `False` for `!=`, because of their *reflexive/irreflexive* property. But the same reason applies to `<` and `>` which are irreflexive, and to `<=` and `>=` which are reflexive. – Géry Ogam Oct 03 '20 at 22:32
  • Python even goes further: when the operands are *not identical*, it returns `False` for `==` and `True` for `!=`, which this time is *arbitrary*. It could as well have raised `TypeError`. To me all operators should choose a single behaviour: 1. Never answer by always raising `TypeError`. 2. Answer only non-arbitrarily by leveraging the reflexivity/irreflexivity properties, otherwise raising `TypeError`. 3. Always answer, by leveraging the reflexivity/irreflexivity properties, otherwise arbitrarily. The problem is that Python does a mix: `==` and `!=` do 3, and `<`, `>`, `<=` and `>=` do 1. – Géry Ogam Oct 03 '20 at 22:52
  • Guido gave a very interesting feedback [here](https://discuss.python.org/t/add-missing-default-implementations-of-le-and-ge/5327/18?u=maggyero) on Python Ideas, in case you are interested. – Géry Ogam Oct 15 '20 at 08:45
  • 1
    @Maggyero yes, that is an interesting discussion. And thanks for posting this on Python Ideas, that’s the by far the most appropriate venue for a discussion of this kind. My regret now is that I had not made the Python 2 history (including the fact that rich comparisons came later, we started with just `__cmp__`) in my answer. – Martijn Pieters Oct 15 '20 at 09:11
5

TLDR: The comparisons operators are not required to return bool. This means that results may not strictly adhere to "a <= b is a < b or a == b" or similar relations. Most importantly, the boolean or may fail to preserve their semantics.

Automatically generating special methods may silently lead to wrong behaviour, similar to how automatic __bool__ is not generally applicable. (This example also treats <= etc. as more than bool.)


An example is expressing time points via comparison operators. For example, the usim simulation framework (disclaimer: I maintain this package) defines time points that can be checked and waited for. We can use comparisons to describe "at or after" some point in time:

  • time > 2000 after 2000.
  • time == 2000 at 2000.
  • time >= 2000 at or after 2000.

(The same applies to < and ==, but the restriction is more difficult to explain.)

Notably, there are two features to each expression: Whether it is satisfied right now (bool(time >= 2000)) and when it will be satisfied (await (time >= 2000)). The first can obviously be evaluated for every case. However, the second cannot.

Waiting for == and >= can be done by waiting for/sleeping until an exact point in time. However, waiting for > requires waiting for a point in time plus some infinitely small delay. The latter cannot be accurately expressed, since there is no generic infinitely small but non-zero number for contemporary number types.

As such, the result of == and >= is fundamentally of a different kind than >. Deriving >= as "> or ==" would be wrong. Thus, usim.time defines == and >= but not > to avoid errors. Automatically defining comparison operators would prevent this, or wrongly define the operators.

MisterMiyagi
  • 44,374
  • 10
  • 104
  • 119
  • Interesting. But note that: 1. I don’t propose to define < or > as you stated, but ≤ and ≥. 2. Assuming you wanted ≤ or ≥ to be undefined instead, you could always override them with `return NotImplemented` (currently this is their implementation). So I don’t see the issue. – Géry Ogam Mar 05 '20 at 10:02
  • 2
    @Maggyero And this kind of reasoning is precisely why I asked what you consider an acceptable answer. For *every* case, one could potentially add some *more* scaffolding to have an escape hatch to the current default. – MisterMiyagi Mar 05 '20 at 10:37
  • @Maggyero As for 1., deriving ``<=`` from ``< or ==`` would be wrong in this case. That ``<=`` is already manually defined does not change that. – MisterMiyagi Mar 05 '20 at 10:42
  • "The comparisons operators are not required to return `bool`." I agree. "This means that results may not strictly adhere to "`<=` is `<` or `==`" or similar relations." I disagree. The [documentation](https://docs.python.org/3/reference/datamodel.html#object.__lt__) states: "By convention, `False` and `True` are returned for a successful comparison. However, these methods can return any value, so if the comparison operator is used in a Boolean context (e.g., in the condition of an `if` statement), Python will call `bool()` on the value to determine if the result is true or false." – Géry Ogam Mar 05 '20 at 11:20
  • Does your `usim` package respects the quoted paragraph in the documentation? Unfortunately I am not familiar with asynchronous programming so I cannot check myself. But according to your conclusion "This means that results may not strictly adhere to "`<=` is `<` or `==`" or similar relations." it does not look so. However I would like to be sure. – Géry Ogam Mar 05 '20 at 11:47
  • @Maggyero Can you clarify your disagreement? The docs you quoted only specify that ``bool`` is called to *convert* results to that type. Other than that, it does not make any mention about the required semantics of the results. In specific, it does say "There are no other implied relationships among the comparison operators, for example, **the truth of (x – MisterMiyagi Mar 05 '20 at 12:22
  • @Maggyero Can you please clarify your confusing about ``usim`` not adhering to the quoted section? The section only states that ``bool`` is called on the result. As hinted at in my answer, it is *impossible* for a type not to implement ``__bool__``. If you mean whether the result of ``bool(time #op# value)`` matches total ordering *for the operators provided*, yes it does. – MisterMiyagi Mar 05 '20 at 12:26
  • I am trying to understand your TLDR reasoning: if `self.__lt__(other)` or `self.__eq__(other)` does not return a `bool`, then `self.__le__(other) == self.__lt__(other) or self.__eq__(other)` is not valid anymore (assuming it were valid for `bool` values of course). Since the `bool()` function is called anyway like stated in the documentation, I don’t understand this implication. Could you clarify? – Géry Ogam Mar 05 '20 at 12:51
  • @Maggyero ``bool`` is only called in an explicitly boolean context, e.g. ``if`` or ``while``. It is *not* called outside of a boolean context, e.g. ``await``, ``for``, ``with``, ``[]``, ... Note how in the linked QA ``numpy`` uses ``>=`` to derive a *mask*, aka an array/matrix/... of booleans, instead of a single boolean. These masks are not meaningful in a boolean context (``__bool__`` throws an error), but can be used for "indexing" via ``[]``. – MisterMiyagi Mar 05 '20 at 12:58
  • But `self.__lt__(other) or self.__eq__(other)` _is_ a Boolean context, so I still don’t understand why you are stating that the union relationship `self.__le__(other) == self.__lt__(other) or self.__eq__(other)` becomes invalid when `self.__lt__(other)` or `self.__eq__(other)` does not return a Boolean value. – Géry Ogam Mar 05 '20 at 17:26
  • 1
    @Maggyero I don't understand you either. Yes, it is a boolean context, no, it does not say anything about ``__lt__`` or ``__eq__``. In the expression ``self.__lt__(other) or self.__eq__(other)``, ``or`` will call ``bool`` on the result of the first expression. Since ``bool`` takes care of providing a boolean, ``__lt__`` is not under any obligation to return a ``bool``. There is no obligation to use ``__lt__`` or ``__eq__`` in such an ``or`` expression either. Also, there is nothing in Python saying that ``self.__le__(other) == self.__lt__(other) or self.__eq__(other)`` must be true. – MisterMiyagi Mar 05 '20 at 17:33
  • 2
    @Maggyero Please reconsider your line of reasoning. So far your only only argument why Python's operators cannot possibly deviate from the union relationship is the union relationship itself, phrased in various ways. The fact is that Python's operators are not required to adhere to the union relationship, making any required appeals to consistency with the union relationship void. – MisterMiyagi Mar 05 '20 at 17:50
  • It seems that we only disagree on the non-Boolean case. For the Boolean case, the union relationship always holds true for `__le__` and `__ge__` and I think you agree, don’t you? So now let’s focus on the non-Boolean case. To me the union relationship `self.__le__(other) == self.__lt__(other) or self.__eq__(other)` should still hold true _after Boolean conversion_ of each subexpression, that is to say `bool(self.__le__(other))`, `bool(self.__lt__(other))`, `bool(self.__eq__(other))`. – Géry Ogam Mar 05 '20 at 19:00
  • 1
    @Maggyero I think it is good API design to satisfy the expected relations, but there is absolutely no reason why it *should*. It might, and in many cases it does, but in general it does not. Most importantly, it is irrelevant because the comparison operators *are not strictly boolean* in Python. They might, and in many cases they are, but in general they are not. – MisterMiyagi Mar 05 '20 at 19:30
  • […] But before Boolean conversion, that union relationship does not always hold. If a user wants to use non-Boolean operators, he should not rely on their default implementations but implement them explicitly. Now I think I get your point: in my proposed `__le__` implementation (`self.__lt__(other) or self.__eq__(other)`), the `or` operator returns the first truthy operand, so not necessarily a Boolean value. Since that relationship only holds for Boolean values, this might be unexpected and arbitrary to return a non-Boolean value (the first truthy operand here). – Géry Ogam Mar 05 '20 at 20:04
  • Thus maybe a less surprising default implementation of `__le__` could only return _Boolean values_ (no arbitrary non-Boolean values would be returned by default): `bool(self.__lt__(other)) or bool(self.__eq__(other))`. That way, if a user wants to return non-Boolean values he has to override the default implementation. And that is consistent with what `__ne__` already does by default: it always returns Boolean values since it is implemented as `not self.__eq__(other)`. So it seems that my suggestion to add a default `__le__` and `__ge__` implementation is still fine, what do you think? – Géry Ogam Mar 05 '20 at 23:18
  • 2
    @Maggyero I think automatically generating special methods is a very poor design. I‘ve specifically linked to an example of a much simpler special method that is automatically present and thus a pain to circumvent. The ecosystem is rife with tools that do not handle this correctly, even though it is part of one of the most prominent third party libraries. On top, there is absolutely no reason to hardwire this *provably wrong behavior* into Python, since it can very easily be applied via decorators or inheritance as required, on a case by case basis. – MisterMiyagi Mar 06 '20 at 05:58
  • So according to you, the fact that `__ne__` is defined by default in Python is a poor design? – Géry Ogam Mar 12 '20 at 18:03
  • 1
    @Maggyero Please strongly consider whether you want to use people that are trying to help you in order to cherry-pick selective answers. The direct answer is "No, this is not the case according to me since I did not make any such claim regarding ``__ne__``." The answer to the question you haven't asked is "Yes, though not even remotely on the same scale as automatically defining comparison operators. Negation being just one additional opcode makes it practical, and Python isn't such a pure language that purity necessarily beats significant practicality." – MisterMiyagi Mar 12 '20 at 19:12
  • That's it, I'm out. Best of luck to you. – MisterMiyagi Mar 12 '20 at 19:53
  • Let me recap. In the Boolean case, some mathematical relationships between comparison operators are always valid so they should be implemented, either by default or by the user (= as the converse of itself, ≠ as the converse of itself, < and > as each other’s converse, ≤ and ≥ as each other’s converse, ≠ as the complement of =, ≤ as the union of < and =, ≥ as the union of > and =). In the non-Boolean case however, not necessarily, since the semantics of the comparison operators is user-defined. – Géry Ogam Mar 12 '20 at 20:20
  • The question is, should we implement these mathematical relationships by default? Since 99% of the practical usage of the comparison operators is in the Boolean case, I am still failing to see how providing these default implementations (which can always be overriden) is a bad design. Especially as most of these default implementations are already part of Python: = is the converse of itself, ≠ is the converse of itself, < and > are each other’s converse, ≤ and ≥ as each other’s converse, ≠ is the complement of =. So why excluding these two: ≤ as the union of < and =, ≥ as the union of > and =? – Géry Ogam Mar 12 '20 at 20:21
  • I was updating my comments. That’s a shame that you gave up the debate. People usually do this when they have no more argument (like @deceze). At least the discussion was interesting with you and I could better articulate my position. I am now more convince than before that my Python improvement suggestion was right. Thanks. – Géry Ogam Mar 12 '20 at 20:35