# Add missing default implementations of \_\_le\_\_ and \_\_ge\_\_

**URL:** <https://discuss.python.org/t/add-missing-default-implementations-of-le-and-ge/5327>\
**Category:** Ideas\
**Created:** [September 27, 2020, 12:12pm UTC](https://discuss.python.org/t/add-missing-default-implementations-of-le-and-ge/5327 "2020-09-27T12:12:50Z")\
**Posts on this page:** 18\
**Page:** 1

<div class="post-metadata">

**Author:** ![maggyero](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/maggyero/32/1505_2.png) [@maggyero](https://discuss.python.org/u/maggyero)\
**Post date:** [September 27, 2020, 12:12pm UTC](https://discuss.python.org/t/add-missing-default-implementations-of-le-and-ge/5327/1 "2020-09-27T12:12:51Z")

</div>

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 is the reason of this post_):

- 2 [complementary](https://en.wikipedia.org/wiki/Binary_relation#Complement) relationships: “= and ≠ are each other’s complement”;
- 6 [converse](https://en.wikipedia.org/wiki/Binary_relation#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](https://en.wikipedia.org/wiki/Binary_relation#Union) relationships: “≤ is the union \< and =” and “≥ is the union of \> and =”.

The following relationships between comparison relations are only valid for [total orders](https://en.wikipedia.org/wiki/Binary_relation#Properties) 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`](https://docs.python.org/3/library/functools.html#functools.total_ordering) provided by the Python standard library):

- 4 [complementary](https://en.wikipedia.org/wiki/Binary_relation#Complement) relationships: “\< and ≥ are each other’s complement” and “\> and ≤ are each other’s complement”.

**So Python is only lacking the 2 union relationships above (“≤ is the union \< and =” and “≥ is the union of \> and =”), which seems arbitrary.**

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__ `, for instance like these (but probably in C for performance, like ` __ne__ `):

```python
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](https://github.com/pika/pika/blob/1.1.0/pika/adapters/select_connection.py#L198)).

Here is the paragraph of the [Python documentation](https://docs.python.org/3/reference/datamodel.html#object. __lt__ ) 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](https://docs.python.org/3/reference/datamodel.html#the-standard-type-hierarchy).

---

<div class="post-metadata">

**Author:** ![guido](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/guido/32/21_2.png) [@guido](https://discuss.python.org/u/guido)\
**Post date:** [September 27, 2020, 7:55pm UTC](https://discuss.python.org/t/add-missing-default-implementations-of-le-and-ge/5327/2 "2020-09-27T19:55:22Z")

</div>

It’s not entirely arbitrary. The two union relationships are the only ones that would have to be implemented by making _two_ calls.

Note however that your version always makes both calls, whereas an optimal version should return a result if the outcome can be determined from the first call alone, e.g. if `x<y` is true, then `x<=y` can return True without testing `x==y`.

One concern I would have is deciding which of the two calls (e.g. `<` or `=`) should be made first.

Another concern is that if you are comparing two sequences using “lexicographic” ordering, your implementation would incur two loops, whereas an optimal version loops once until it finds a pair of elements that are not equal. Something like your version (even if optimized to skip the second call if possible) would encourage lazy developers to produce inefficient code.

---

<div class="post-metadata">

**Author:** ![maggyero](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/maggyero/32/1505_2.png) [@maggyero](https://discuss.python.org/u/maggyero)\
**Post date:** [September 28, 2020, 3:03pm UTC](https://discuss.python.org/t/add-missing-default-implementations-of-le-and-ge/5327/3 "2020-09-28T15:03:30Z")

</div>

Thanks for your insight @guido, you raised important questions.

About the first concern, an argument for calling ` __eq__ ` before ` __lt__ ` or ` __gt__ ` that comes to mind is that ` __eq__ ` is more frequently implemented than ` __lt__ ` or ` __gt__ `. So the optimised default implementations of ` __le__ ` and ` __ge__ ` would look like these:

```python
def __le__ (self, other):
    result_1 = self. __eq__ (other)
    if result_1 is True:
        return True
    result_2 = self. __lt__ (other)
    if result_2 is True:
        return True
    if result_1 is False and result_2 is False:
        return False
    return NotImplemented

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

```

About the second concern, I agree that these default implementations would not be optimised for all cases like lexicographic ordering. Now I am wondering if Python already provides reasonable default implementations in other parts of the language or if that would be the first occurrence. And if the benefits (not having to implement them every time) of default implementations would outweigh their drawbacks (lazy programmers always relying on them instead of optimising when necessary). Do you have other default implementation examples in mind? (I cannot think of one right now.)

---

<div class="post-metadata">

**Author:** ![brettcannon](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/brettcannon/32/34895_2.png) [@brettcannon](https://discuss.python.org/u/brettcannon)\
**Post date:** [September 28, 2020, 8:54pm UTC](https://discuss.python.org/t/add-missing-default-implementations-of-le-and-ge/5327/4 "2020-09-28T20:54:09Z")

</div>

> [@maggyero](#):
>
> ` __eq__ ` is more frequently implemented

` __eq__ ` is actually **always** implemented in some form due to being made available on `object` (same with ` __ne__ ()`.

You also cannot guarantee a return value of `True` or `False` as the return values can be anything. You also need to take into consideration `NotImplemented`.

Lastly, my reading of the [data model for rich comparisons](https://docs.python.org/3/reference/datamodel.html#object. __ge__ ) does not suggest anything other than ` __eq__ ` and ` __ne__ ` being expected to be implemented on `object` (I think everything else just automatically returns `NotImplemented`). I believe the existence of the other methods on `object` are a side-effect of how Python handles things at the C level – there’s a single C function to implement all comparison functions – and thus can’t differentiate nor guarantee that the other methods do exist.

Now that doesn’t mean this couldn’t be done for the syntactic operations, but if this is going to be expected of the methods then the data model may need expanding to also say that `object` defines all the other methods as well (or at least be clear about that if that’s already the expectation).

---

<div class="post-metadata">

**Author:** ![guido](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/guido/32/21_2.png) [@guido](https://discuss.python.org/u/guido)\
**Post date:** [September 28, 2020, 9:11pm UTC](https://discuss.python.org/t/add-missing-default-implementations-of-le-and-ge/5327/5 "2020-09-28T21:11:50Z")

</div>

I personally think this should not be changed. It would likely cause a mountain of subtle backward incompatibilities, for very little benefit.

---

<div class="post-metadata">

**Author:** ![maggyero](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/maggyero/32/1505_2.png) [@maggyero](https://discuss.python.org/u/maggyero)\
**Post date:** [September 28, 2020, 11:35pm UTC](https://discuss.python.org/t/add-missing-default-implementations-of-le-and-ge/5327/6 "2020-09-28T23:35:04Z")

</div>

> [@brettcannon](#):
>
> ` __eq__ ` is actually **always** implemented in some form due to being made available on `object` (same with ` __ne__ ()` .

By “implemented” I actually meant by the user (overridden).

> [@brettcannon](#):
>
> You also cannot guarantee a return value of `True` or `False` as the return values can be anything. You also need to take into consideration `NotImplemented` .

The given optimised default implementations of ` __le__ ` and ` __ge__ ` do take into consideration `NotImplemented`, by returning `NotImplemented` when there is no answer to the disjunction of `result_1` and `result_2`, that is when `(result_1, result_2)` is `(NotImplemented, NotImplemented)` or `(False, NotImplemented)` or `(NotImplemented, False)`.

As for the cases when ` __eq__ ` has been overridden to return a non-Boolean value, the given optimised default implementations of ` __le__ ` and ` __ge__ ` should of course be also overridden since, like ` __ne__ ` which is also defined in terms of ` __eq__ ` and therefore should be overridden as well, they cannot anticipate the user semantics.

> [@brettcannon](#):
>
> Lastly, my reading of the [data model for rich comparisons](https://docs.python.org/3/reference/datamodel.html#object. __ge__ ) does not suggest anything other than ` __eq__ ` and ` __ne__ ` being expected to be implemented on `object` (I think everything else just automatically returns `NotImplemented` ).

I have the same understanding, except for ` __eq__ ` which I think also returns `NotImplemented` by default. Since as explained in the [Python documentation](https://docs.python.org/3/reference/datamodel.html#object. __lt__ ):

> 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`.

Which can be translated into Python code as follows:

```python
def __eq__ (self, other):
    return NotImplemented

def __ne__ (self, other):
    result = self. __eq__ (other)
    if result is not NotImplemented:
        return not result
    return NotImplemented

def __lt__ (self, other):
    return NotImplemented

def __gt__ (self, other):
    return NotImplemented

def __le__ (self, other):
    return NotImplemented

def __ge__ (self, other):
    return NotImplemented

```

But those are the default comparison _method_ implementations. So the 2 complementary relationships (“= and ≠ are each other’s complement”) are implemented at the method level.

Now my understanding of how the comparison _operator_ implementations (in C) delegate to the previous methods is as follows (translated into Python code with the names `eq` for `==`, `ne` for `!=`, `lt` for `<`, `gt` for `>`, `le` for `<=` and `ge` for `>=`):

```python
def eq(left, right):
    if type(left) != type(right) and isinstance(right, type(left)):
        result = right. __eq__ (left)
        if result is NotImplemented:
            result = left. __eq__ (right)
    else:
        result = left. __eq__ (right)
        if result is NotImplemented:
            result = right. __eq__ (left)
    if result is NotImplemented:
        result = left is right
    return result

def ne(left, right):
    if type(left) != type(right) and isinstance(right, type(left)):
        result = right. __ne__ (left)
        if result is NotImplemented:
            result = left. __ne__ (right)
    else:
        result = left. __ne__ (right)
        if result is NotImplemented:
            result = right. __ne__ (left)
    if result is NotImplemented:
        result = left is not right
    return result

def lt(left, right):
    if type(left) != type(right) and isinstance(right, type(left)):
        result = right. __gt__ (left)
        if result is NotImplemented:
            result = left. __lt__ (right)
    else:
        result = left. __lt__ (right)
        if result is NotImplemented:
            result = right. __gt__ (left)
    if result is NotImplemented:
        raise TypeError(
            f"'<' not supported between instances of '{type(left). __name__ }' "
            f"and '{type(right). __name__ }'"
        )
    return result

def gt(left, right):
    if type(left) != type(right) and isinstance(right, type(left)):
        result = right. __lt__ (left)
        if result is NotImplemented:
            result = left. __gt__ (right)
    else:
        result = left. __gt__ (right)
        if result is NotImplemented:
            result = right. __lt__ (left)
    if result is NotImplemented:
        raise TypeError(
            f"'>' not supported between instances of '{type(left). __name__ }' "
            f"and '{type(right). __name__ }'"
        )
    return result

def le(left, right):
    if type(left) != type(right) and isinstance(right, type(left)):
        result = right. __ge__ (left)
        if result is NotImplemented:
            result = left. __le__ (right)
    else:
        result = left. __le__ (right)
        if result is NotImplemented:
            result = right. __ge__ (left)
    if result is NotImplemented:
        raise TypeError(
            f"'<=' not supported between instances of '{type(left). __name__ }' "
            f"and '{type(right). __name__ }'"
        )
    return result

def ge(left, right):
    if type(left) != type(right) and isinstance(right, type(left)):
        result = right. __le__ (left)
        if result is NotImplemented:
            result = left. __ge__ (right)
    else:
        result = left. __ge__ (right)
        if result is NotImplemented:
            result = right. __le__ (left)
    if result is NotImplemented:
        raise TypeError(
            f"'>=' not supported between instances of '{type(left). __name__ }' "
            f"and '{type(right). __name__ }'"
        )
    return result

```

So @brettcannon to me the fall back of `==` (equality comparison) to `is` (identity comparison) is not implemented at the method level (` __eq__ `) but at the operator level (`==`, which I named `eq` in the last snippet).

And the 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”) are also implemented at the operator level (cf. in each operator the fallback on the converse method of the other operand when the method of the first operand returns `NotImplemented` in the last snippet).

By the way, in addition to the missing implementations at the method level (` __le__ ` and ` __ge__ `) of the 2 union relationships (“≤ is the union \< and =” and “≥ is the union of \> and =”) that we have been discussing, I realised this afternoon that another thing is missing. In the [Python documentation](https://docs.python.org/3/reference/expressions.html#value-comparisons), the _reflexivity_ argument is given for motivating the default implementation of the comparison operator `==` (at the operator level) as an identity comparison (cf. the last snippet where `==`, which I named `eq`, delegates to `is` as a last resort):

> 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 **[incorrect for `<=`, and `>=` which are also reflexive!]**.

But this reflexivity argument also applies to the comparison operators `<=` and `>=` since they are also _reflexive_ (as they are partial order, that is reflexive, antisymmetric and transitive)—but it does not apply to `<` and `>` since they are not reflexive (as they are strict order, that is irreflexive, antisymmetric and transitive) nor to `!=` since it is not reflexive (it is only symmetric). Indeed, `x is y` implies `x <= y` and `x >= y`.

This means that instead of raising a `TypeError` when the converse methods of both operands return `NotImplemented`, `<=` and `>=` (named `le` and `ge` in the last snippet) should also delegate to `is` as a last resort (like `==`, named `eq` in the last snippet, already does):

```python
def le(left, right):
    if type(left) != type(right) and isinstance(right, type(left)):
        result = right. __ge__ (left)
        if result is NotImplemented:
            result = left. __le__ (right)
    else:
        result = left. __le__ (right)
        if result is NotImplemented:
            result = right. __ge__ (left)
    if result is NotImplemented:
        result = left is right # instead of `raise TypeError([…])`
    return result

def ge(left, right):
    if type(left) != type(right) and isinstance(right, type(left)):
        result = right. __le__ (left)
        if result is NotImplemented:
            result = left. __ge__ (right)
    else:
        result = left. __ge__ (right)
        if result is NotImplemented:
            result = right. __le__ (left)
    if result is NotImplemented:
        result = left is right # instead of `raise TypeError([…])`
    return result

```

---

<div class="post-metadata">

**Author:** ![brettcannon](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/brettcannon/32/34895_2.png) [@brettcannon](https://discuss.python.org/u/brettcannon)\
**Post date:** [September 29, 2020, 7:27pm UTC](https://discuss.python.org/t/add-missing-default-implementations-of-le-and-ge/5327/7 "2020-09-29T19:27:04Z")

</div>

I should mention I am in the middle of writing a blog post that outlines how Python does rich comparisons, so if you want links to the C source and example Python code you can wait for that to come out.

> [@maggyero](#):
>
> So @brettcannon to me the fall back of `==` (equality comparison) to `is` (identity comparison) is not implemented at the method level ( ` __eq__ ` ) but at the operator level ( `==` , which I named `eq` in the last snippet).

It’s actually implemented at both the [operator level](https://github.com/python/cpython/blob/v3.8.3/Objects/object.c#L672) and the [object level](https://github.com/python/cpython/blob/v3.8.3/Objects/typeobject.c#L3834-L3880).

> [@maggyero](#):
>
> And the 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”) are also implemented at the operator level

Correct.

> [@guido](#):
>
> I personally think this should not be changed. It would likely cause a mountain of subtle backward incompatibilities, for very little benefit.

I agree; I think practicality beats purity in this instance.

---

<div class="post-metadata">

**Author:** ![maggyero](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/maggyero/32/1505_2.png) [@maggyero](https://discuss.python.org/u/maggyero)\
**Post date:** [September 29, 2020, 10:32pm UTC](https://discuss.python.org/t/add-missing-default-implementations-of-le-and-ge/5327/8 "2020-09-29T22:32:36Z")

</div>

> [@brettcannon](#):
>
> I should mention I am in the middle of writing a blog post that outlines how Python does rich comparisons, so if you want links to the C source and example Python code you can wait for that to come out.

That will be awesome as I haven’t read the C source code myself (I deduced the above model for comparison operations only from the documentation and some testing). Please notify me when you publish your article, I have been really enjoying reading your Snarky blog. The funny thing is that I was sure you were preparing an article on this subject after reading you article on attribute access. If I remember well, you are writing a series on the Python data model as part of a project on a Rust implementation of Python, aren’t you?

> [@brettcannon](#):
>
> It’s actually implemented at both the [operator level](https://github.com/python/cpython/blob/v3.8.3/Objects/object.c#L672) and the [object level](https://github.com/python/cpython/blob/v3.8.3/Objects/typeobject.c#L3834-L3880).

Thanks for the links to the C source code! So what is the point of the implementation of the identity fallback at the object level since it is already implemented at the operator level?

> [@guido](#):
>
> I personally think this should not be changed. It would likely cause a mountain of subtle backward incompatibilities, for very little benefit.

Thanks for your feedback on the 2 missing _union relationships_ “`<=` is the union `<` and `=`” and “`>=` is the union of `>` and `=`”.

And do you also have the same opinion about the missing _identity fallback_ (instead of raising a `TypeError`) for `<=` and `>=` (since they are reflexive like `==`, that is `x is y` implies `x <= y` and `x >= y`) that I mentioned in my last comment?

---

<div class="post-metadata">

**Author:** ![brettcannon](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/brettcannon/32/34895_2.png) [@brettcannon](https://discuss.python.org/u/brettcannon)\
**Post date:** [September 29, 2020, 11:44pm UTC](https://discuss.python.org/t/add-missing-default-implementations-of-le-and-ge/5327/9 "2020-09-29T23:44:55Z")

</div>

> [@maggyero](#):
>
> Please notify me when you publish your article

I’ll try to remember, but you can also subscribe to the RSS/Atom feed or sign up for the newsletter if you want to be sure to find out.

> [@maggyero](#):
>
> I have been really enjoying reading your Snarky blog.

Thanks! Glad you have been enjoying the posts.

> [@maggyero](#):
>
> If I remember well, you are writing a series on the Python data model as part of a project on a Rust implementation of Python, aren’t you?

It’s primarily to document what the bare minimum Python syntax that must be implemented is, and implementing everything else in Python code (e.g. functions for rich comparisons, but probably can’t get rid of `if`). Basically it’s what does the compiler really need to know about compared to the parser (via AST transforms)?

> [@maggyero](#):
>
> Thanks for the links to the C source code! So what is the point of the implementation of the identity fallback at the object level since it is already implemented at the operator level?

So what if you called `object. __eq__ (a, b)` instead of `a == b`? You could just blindly return `NotImplemented` in the former case, but it’s also very cheap to implement the appropriate logic, so why not? Pragmatically, when [implementing `object. __eq__ ` and ` __ne__ ` is pretty small](https://github.com/brettcannon/desugar/blob/8946864315f19dfa941d9812801baa2148f36a74/desugar/builtins.py#L119-L136) is that simple then it just makes sense to have it implemented anyway.

---

<div class="post-metadata">

**Author:** ![maggyero](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/maggyero/32/1505_2.png) [@maggyero](https://discuss.python.org/u/maggyero)\
**Post date:** [September 30, 2020, 9:31pm UTC](https://discuss.python.org/t/add-missing-default-implementations-of-le-and-ge/5327/10 "2020-09-30T21:31:59Z")

</div>

> [@brettcannon](#):
>
> So what if you called `object. __eq__ (a, b)` instead of `a == b`?

Oh I see, thanks! By the way, is there any particular reason you wrote:

```python
return id(self) == id(other) or NotImplemented

```

instead of just:

```python
return self is other or NotImplemented

```

in your `object. __eq__ ` implementation (I thought it was equivalent)? Same question for:

```python
result = not bool(result)

```

instead of just:

```python
result = not result

```

in your `object. __ne__ ` implementation.

And what is your opinion about having the same identity fallback than `==` for `<=` and `>=` (since they are reflexive like `==`, that is `x is y` implies `x <= y` and `x >= y`) at the operator level, and now that we are talking about it, also having the same identity fallback than ` __eq__ ` at the method level for ` __le__ ` and ` __ge__ `?

---

<div class="post-metadata">

**Author:** ![brettcannon](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/brettcannon/32/34895_2.png) [@brettcannon](https://discuss.python.org/u/brettcannon)\
**Post date:** [September 30, 2020, 10:27pm UTC](https://discuss.python.org/t/add-missing-default-implementations-of-le-and-ge/5327/11 "2020-09-30T22:27:53Z")

</div>

> [@maggyero](#):
>
> is there any particular reason you wrote:
> 
> ```auto
> return id(self) == id(other) or NotImplemented
> 
> ```
> 
> instead of just:
> 
> ```auto
> return self is other or NotImplemented
> 
> ```
> 
> in your `object. __eq__ ` implementation (I thought it was equivalent)?

Yes, because I have not implemented `is` yet in my project. 😉 Remember the whole point is to try and find what syntax or semantics must be supported in Python. Same goes for the explicit call to `bool()`; I have not analyzed the `not` keyword to make sure it is actually calling `bool()` underneath the hood.

> [@maggyero](#):
>
> And what is your opinion about having the same identity fallback than `==` for `<=` and `>=`

> [@maggyero](#):
>
> also having the same identity fallback than ` __eq__ ` at the method level for ` __le__ ` and ` __ge__ ` ?

It doesn’t work the same because you can’t provide default implementations that won’t just end up being an infinite loop trying to call the inverse of each other. With ` __eq__ ` and ` __ne__ ` you can guarantee that at least ` __eq__ ` will return _something_.

---

<div class="post-metadata">

**Author:** ![maggyero](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/maggyero/32/1505_2.png) [@maggyero](https://discuss.python.org/u/maggyero)\
**Post date:** [October 1, 2020, 10:08am UTC](https://discuss.python.org/t/add-missing-default-implementations-of-le-and-ge/5327/12 "2020-10-01T10:08:36Z")

</div>

> [@brettcannon](#):
>
> Yes, because I have not implemented `is` yet in my project. 😉 Remember the whole point is to try and find what syntax or semantics must be supported in Python. Same goes for the explicit call to `bool()` ; I have not analyzed the `not` keyword to make sure it is actually calling `bool()` underneath the hood.

Alright. 🙂

> [@brettcannon](#):
>
> It doesn’t work the same because you can’t provide default implementations that won’t just end up being an infinite loop trying to call the inverse of each other. With ` __eq__ ` and ` __ne__ ` you can guarantee that at least ` __eq__ ` will return _something_ .

I am not sure we are talking about the same thing. I should have been more specific, let me explain better.

At the _operator_ level (`<=` and `>=`), I meant (translated into Python, with `le` for `<=` and `ge` for `>=`):

```python
def le(left, right):
    if type(left) != type(right) and isinstance(right, type(left)):
        result = right. __ge__ (left)
        if result is NotImplemented:
            result = left. __le__ (right)
    else:
        result = left. __le__ (right)
        if result is NotImplemented:
            result = right. __ge__ (left)
    if result is NotImplemented:
        result = left is right # instead of `raise TypeError([…])`
    return result

```

and

```python
def ge(left, right):
    if type(left) != type(right) and isinstance(right, type(left)):
        result = right. __le__ (left)
        if result is NotImplemented:
            result = left. __ge__ (right)
    else:
        result = left. __ge__ (right)
        if result is NotImplemented:
            result = right. __le__ (left)
    if result is NotImplemented:
        result = left is right # instead of `raise TypeError([…])`
    return result

```

At the _method_ level (` __le__ ` and ` __ge__ `), I meant (translated into Python):

```python
def __le__ (self, other):
    return self is other or NotImplemented # instead of `return NotImplemented`

```

and

```python
def __ge__ (self, other):
    return self is other or NotImplemented # instead of `return NotImplemented`

```

All these default identity fallbacks would be valid since `x is y` implies `x <= y`, `x >= y`, `x. __le__ (y)` and `x. __ge__ (y)`. **That is if one evaluates `x <= x`, `x >= x`, `x. __le__ (x)` or `x. __ge__ (x)`, he expects Python to return `True`, not to raise `TypeError` for the first two and return `NotImplemented` for the last two like currently.**

In other words, the binary relations ≤ and ≥ are _reflexive_ but this property is not currently taken into account by Python, which only takes into account the reflexivity of the binary relation =. Note that these default identity fallbacks are orthogonal to the 2 union relationships that we have been initially discussing in this thread. That is why I then asked @guido his opinion on this different subject (he only gave his opinion on the 2 union relationships so far, raising backward incompatibility concerns).

_Remark._ — Just for completeness, if we were to add to Python _both_ the default identity fallback (at the operator and method levels) and the 2 union relationships (at the method level with @guido’s optimised version)—which would be the ideal—, the previous ` __le__ ` and ` __ge__ ` implementations in this answer would become:

```python
def __le__ (self, other):
    if self is other:
        return True
    result_1 = self. __eq__ (other)
    if result_1 is True:
        return True
    result_2 = self. __lt__ (other)
    if result_2 is True:
        return True
    if result_1 is False and result_2 is False:
        return False
    return NotImplemented

```

and

```python
def __ge__ (self, other):
    if self is other:
        return True
    result_1 = self. __eq__ (other)
    if result_1 is True:
        return True
    result_2 = self. __gt__ (other)
    if result_2 is True:
        return True
    if result_1 is False and result_2 is False:
        return False
    return NotImplemented

```

---

<div class="post-metadata">

**Author:** ![brettcannon](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/brettcannon/32/34895_2.png) [@brettcannon](https://discuss.python.org/u/brettcannon)\
**Post date:** [October 1, 2020, 9:47pm UTC](https://discuss.python.org/t/add-missing-default-implementations-of-le-and-ge/5327/13 "2020-10-01T21:47:49Z")

</div>

> [@maggyero](#):
>
> **That is if one evaluates `x <= x` , `x >= x` , `x. __le__ (x)` or `x. __ge__ (x)` , he expects Python to return `True` , not to raise `TypeError` for the first two and return `NotImplemented` for the last two like currently.**

I still disagree with the idea. I think your assertion that a developer “expects Python to return `True`” doesn’t universally hold as I certainly don’t expect it to work as the `>` will potentially raise `TypeError`.

Anyway, I appreciate the ideas but I am sticking by my original belief that practicality beats purity here and changing has way too high of a potential backwards-compatibility cost compared to what small benefit we may get from this change.

---

<div class="post-metadata">

**Author:** ![maggyero](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/maggyero/32/1505_2.png) [@maggyero](https://discuss.python.org/u/maggyero)\
**Post date:** [October 3, 2020, 11:37pm UTC](https://discuss.python.org/t/add-missing-default-implementations-of-le-and-ge/5327/14 "2020-10-03T23:37:23Z")

</div>

So far we have been talking about the _reflexibility_ property of:

- `==` (`x == x` evaluates to `True`);
- `<=` (`x <= x` _should_ evaluate to `True` instead of raising `TypeError`);
- `>=` (`x >= x` _should_ evaluate to `True` instead of raising `TypeError`).

But today I have just realised that another thing is missing, we have overlooked the _irreflexibility_ property of:

- `!=` (`x != x` evaluates to `False`);
- `<` (`x < x` _should_ evaluate to `False` instead of raising `TypeError`);
- `>` (`x > x` _should_ evaluate to `False` instead of raising `TypeError`).

Thus not only the reflexive operators `<=` and `>=` (and their associated methods ` __le__ ` and ` __ge__ `) but also the irreflexive operators `<` and `>` (and their associated methods ` __lt__ ` and ` __gt__ `) do not behave as I would have expected by default: when the operands are _identical_, the former do not return `True` while the latter do not return `False`.

Only the reflexive operator `==` (and its associated method ` __eq__ `) and irreflexive operator `!=` (and its associated method ` __ne__ `) behave as I would have expected by default: when the operands are _identical_, the former returns `True` while the latter returns `False`. And Python goes even further: when the operands are _not identical_, the former returns `False` and the latter returns `True`, which this time is _arbitrary_ since it is not a consequence of the properties of the operators. It could have raised `TypeError` as well.

> [@brettcannon](#):
>
> I think your assertion that a developer “expects Python to return `True`” doesn’t universally hold as I certainly don’t expect it to work as the `>` will potentially raise `TypeError`.

I agree with you, there are several possible behaviours (I think there are three) for the default implementations of the comparison operators:

1. **Never answer** (by always raising `TypeError`).
2. **Answer only non-arbitrarily** (by leveraging the reflexivity and irreflexivity properties when possible, otherwise by raising `TypeError`).
3. **Always answer** (by leveraging the reflexivity and irreflexivity properties when possible, otherwise arbitrarily).

But all comparison operators should follow the same behaviour. The problem is that it is currently not the case, Python does a mix: `==` and `!=` follow behaviour 3, while `<`, `>`, `<=` and `>=` follow behaviour 1.

---

<div class="post-metadata">

**Author:** ![brettcannon](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/brettcannon/32/34895_2.png) [@brettcannon](https://discuss.python.org/u/brettcannon)\
**Post date:** [October 5, 2020, 6:56pm UTC](https://discuss.python.org/t/add-missing-default-implementations-of-le-and-ge/5327/15 "2020-10-05T18:56:50Z")

</div>

Just an FYI that I don’t have any more time to dedicate to this topic. I published [https://snarky.ca/unravelling-rich-comparison-operators/](https://snarky.ca/unravelling-rich-comparison-operators/) and I personally don’t have any interest in proposing any changes to Python’s semantics around comparisons.

---

<div class="post-metadata">

**Author:** ![maggyero](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/maggyero/32/1505_2.png) [@maggyero](https://discuss.python.org/u/maggyero)\
**Post date:** [October 5, 2020, 10:08pm UTC](https://discuss.python.org/t/add-missing-default-implementations-of-le-and-ge/5327/16 "2020-10-05T22:08:27Z")

</div>

> [@brettcannon](#):
>
> I published [Unravelling rich comparison operators](https://snarky.ca/unravelling-rich-comparison-operators/)

Thanks for the article, it was a very informative read. This paragraph especially caught my attention as it is relevant to our current discussion:

> Back in the Python 2 days, you could compare any objects using any comparison operator and you would get a result. **But those semantics led to odd cases where bad data in a list, for instance, would still be sortable.** By making only `==` and `!=` always succeed (unless their special methods raise an exception), you prevent such unexpected interactions between objects and having silent errors pass **(although some people wish even _this_ special case for `==` and `!=` didn’t exist)**.

I was not aware that comparison operators in Python 2 never raised `TypeError` by default (if you have a link to the debate that introduced the change in Python 3, please share it, I am intrigued). That is interesting as it provides an argument in favor of behaviours 1 or 2 over behaviour 3 in my last answer. Behaviour 1 has been adopted for `<`, `>`, `<=` and `>=` in Python 3 but behaviour 3 has been kept for `==` and `!=`, which I deplore (so does “some people” refer to me?). Behaviour 2 (for all operators) seems to be the superior solution as it keeps the best of both worlds (behaviours 1 and 3): answer but only when you are sure (that is when the answer is a consequence of the operator’s reflexiblility/irreflexibility property).

> [@brettcannon](#):
>
> I personally don’t have any interest in proposing any changes to Python’s semantics around comparisons.

Initially I had this hope, but since @guido said it might lead to backward incompatibilities without enough benefits, I think I have lost this hope (though he was only referring to adding the _union relationships_ that was originally discussed, not to adding the _reflexibility/irreflexibilty properties_ that was lately discussed, but I assume he has the same opinion on that). And he is probably right, I don’t see myself a lot of benefits besides enforcing the only 2 missing mathematical features of the _default_ comparison operators: their union relationships and reflexivity/irreflexivity properties (all their other mathematical relationships and properties are already enforced by default, which is already quite good).

---

<div class="post-metadata">

**Author:** ![steven.daprano](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/steven.daprano/32/1083_2.png) [@steven.daprano](https://discuss.python.org/u/steven.daprano)\
**Post date:** [October 5, 2020, 10:51pm UTC](https://discuss.python.org/t/add-missing-default-implementations-of-le-and-ge/5327/17 "2020-10-05T22:51:00Z")

</div>

Géry Ogam wrote:

“All comparison operators should follow the same behaviour.”

Why?

"The problem is that it is currently not the case, Python does a mix:

`==` and `!=` follow 3, while `<`, `>`, `<=` and `>=` follow 1."

How is it a problem? What code is broken by the current design, or made

unnecessarily and significantly more difficult?

For context, behaviour 3 is that (in)equality operators have default

implementations somewhat close to:

```
def __eq__ (self, other): return self is other

def __ne__ (self, other): return self is not other

```

while behaviour 3 is that the other comparisons have defaults

somewhat like:

```
def __lt__ (self, other): return NotImplemented

```

When both operands return NotImplemented, TypeError is raised, so

behaviour 3 is _almost_ equivalent to not implementing the dunder method

at all. The difference is that:

```
hasattr(obj, ' __lt__')

```

will return True even if obj does not actually support the `<` operator,

which is mildly inconvenient for “Look Before You Leap” introspection.

---

<div class="post-metadata">

**Author:** ![maggyero](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/maggyero/32/1505_2.png) [@maggyero](https://discuss.python.org/u/maggyero)\
**Post date:** [October 6, 2020, 9:59pm UTC](https://discuss.python.org/t/add-missing-default-implementations-of-le-and-ge/5327/18 "2020-10-06T21:59:21Z")

</div>

For reference, here after is how I see now the implementations (given in Python for clarity instead of C like the actual CPython implementations) of the comparison operators (`==`, `!=`, `<`, `>`, `<=` and `>=`) and their associated default special comparison methods (` __eq__ `, ` __ne__ `, ` __lt__ `, ` __gt__ `, ` __le__ ` and ` __ge__ `) that would add the **missing mathematical features** from the [theory of binary relations](https://en.wikipedia.org/wiki/Binary_relation) that we have been discussing in this thread with @guido and @brettcannon, which are:

- the **union relationships** ≤ is the union of \< and =, and ≥ is the union of \> and =;
- the **irreflexivity properties** of \< and \>;
- the **reflexivity properties** of ≤ and ≥.

Following these implementations is a test suite for which every assertion fails with the current implementations but succeeds with the new implementations. The aim is to show the differences in semantics as well as the possible benefits and drawbacks of the new implementations.

## Implementations of the comparison operators

The differences between the current and new implementations are indicated. The name `eq` stands for `==`, `ne` for `!=`, `lt` for `<`, `gt` for `>`, `le` for `<=` and `ge` for `>=`.

```python
def eq(left, right):
    if type(left) != type(right) and isinstance(right, type(left)):
        result = right. __eq__ (left)
        if result is NotImplemented:
            result = left. __eq__ (right)
    else:
        result = left. __eq__ (right)
        if result is NotImplemented:
            result = right. __eq__ (left)
    if result is NotImplemented:
        # Current implementation: return left is right
        # New implementation:
        raise TypeError(
            "'==' not supported between instances of "
            f"'{type(left). __name__ }' and '{type(right). __name__ }'"
        )
    return result

```

```python
def ne(left, right):
    if type(left) != type(right) and isinstance(right, type(left)):
        result = right. __ne__ (left)
        if result is NotImplemented:
            result = left. __ne__ (right)
    else:
        result = left. __ne__ (right)
        if result is NotImplemented:
            result = right. __ne__ (left)
    if result is NotImplemented:
        # Current implementation: return left is not right
        # New implementation:
        raise TypeError(
            "'!=' not supported between instances of "
            f"'{type(left). __name__ }' and '{type(right). __name__ }'"
        )
    return result

```

```python
def lt(left, right):
    if type(left) != type(right) and isinstance(right, type(left)):
        result = right. __gt__ (left)
        if result is NotImplemented:
            result = left. __lt__ (right)
    else:
        result = left. __lt__ (right)
        if result is NotImplemented:
            result = right. __gt__ (left)
    if result is NotImplemented:
        raise TypeError(
            "'<' not supported between instances of "
            f"'{type(left). __name__ }' and '{type(right). __name__ }'"
        )
    return result

```

```python
def gt(left, right):
    if type(left) != type(right) and isinstance(right, type(left)):
        result = right. __lt__ (left)
        if result is NotImplemented:
            result = left. __gt__ (right)
    else:
        result = left. __gt__ (right)
        if result is NotImplemented:
            result = right. __lt__ (left)
    if result is NotImplemented:
        raise TypeError(
            "'>' not supported between instances of "
            f"'{type(left). __name__ }' and '{type(right). __name__ }'"
        )
    return result

```

```python
def le(left, right):
    if type(left) != type(right) and isinstance(right, type(left)):
        result = right. __ge__ (left)
        if result is NotImplemented:
            result = left. __le__ (right)
    else:
        result = left. __le__ (right)
        if result is NotImplemented:
            result = right. __ge__ (left)
    if result is NotImplemented:
        raise TypeError(
            "'<=' not supported between instances of "
            f"'{type(left). __name__ }' and '{type(right). __name__ }'"
        )
    return result

```

```python
def ge(left, right):
    if type(left) != type(right) and isinstance(right, type(left)):
        result = right. __le__ (left)
        if result is NotImplemented:
            result = left. __ge__ (right)
    else:
        result = left. __ge__ (right)
        if result is NotImplemented:
            result = right. __le__ (left)
    if result is NotImplemented:
        raise TypeError(
            "'>=' not supported between instances of "
            f"'{type(left). __name__ }' and '{type(right). __name__ }'"
        )
    return result

```

## Implementations of the default special comparison methods

The differences between the current and new implementations are indicated.

```python
def __eq__ (self, other):
    return self is other or NotImplemented # reflexivity property

```

```python
def __ne__ (self, other):
    result = self. __eq__ (other)
    if result is not NotImplemented:
        return not result
    return NotImplemented

```

```python
def __lt__ (self, other):
    # Current implementation: return NotImplemented
    # New implementation:
    return self is not other and NotImplemented # irreflexivity property

```

```python
def __gt__ (self, other):
    # Current implementation: return NotImplemented
    # New implementation:
    return self is not other and NotImplemented # irreflexivity property

```

```python
def __le__ (self, other):
    # Current implementation: return NotImplemented
    # New implementation:
    result_1 = self. __eq__ (other)
    if result_1 is True:
        return True # union relationship
    result_2 = self. __lt__ (other)
    if result_2 is True:
        return True # union relationship
    if result_1 is False and result_2 is False:
        return False # union relationship
    return NotImplemented

```

```python
def __ge__ (self, other):
    # Current implementation: return NotImplemented
    # New implementation:
    result_1 = self. __eq__ (other)
    if result_1 is True:
        return True # union relationship
    result_2 = self. __gt__ (other)
    if result_2 is True:
        return True # union relationship
    if result_1 is False and result_2 is False:
        return False # union relationship
    return NotImplemented

```

## Benefits and drawbacks of the new implementations

For the union relationships:

```python
class X:
    def __init__ (self, attr):
        self.attr = attr
    def __eq__ (self, other):
        if isinstance(other, __class__ ):
            return self.attr == other.attr
        return NotImplemented
    def __lt__ (self, other):
        if isinstance(other, __class__ ):
            return self.attr < other.attr
        return NotImplemented
    def __gt__ (self, other):
        if isinstance(other, __class__ ):
            return self.attr > other.attr
        return NotImplemented

x, y = X(3), X(3)

assert x <= y is True # instead of raising a `TypeError`
assert x >= y is True # instead of raising a `TypeError`

assert x. __le__ (y) is True # instead of returning `NotImplemented`
assert x. __ge__ (y) is True # instead of returning `NotImplemented`

```

- **Benefits:** no need to override ` __le__ ` and ` __ge__ ` anymore.
- **Drawbacks:** @guido raised concerns about possible backward compatibility issues.

For the irreflexivity properties:

```python
class X:
    pass

x, y = X(), X()

assert x < x is False # instead of raising a `TypeError`
assert x > x is False # instead of raising a `TypeError`

assert x. __lt__ (x) is False # instead of returning `NotImplemented`
assert x. __gt__ (x) is False # instead of returning `NotImplemented`

try:
    x != y
except TypeError:
    pass # instead of returning `True`
else:
    assert False

```

- **Benefits:** `<`, `>`, ` __lt__ ` and ` __gt__ ` return the only possible result for identical objects, and `!=` does not return an arbitrary result for non-identical objects (and consequently the user is fully in control of the behaviour of `!=` by overriding ` __ne__ ` since `!=` raises a `TypeError` instead of returning an arbitrary fallback when both ` __ne__ ` return `NotImplemented`).
- **Drawbacks:** [to be determined].

For the reflexivity properties:

```python
class X:
    pass

x, y = X(), X()

assert x <= x is True # instead of raising a `TypeError`
assert x >= x is True # instead of raising a `TypeError`

assert x. __le__ (x) is True # instead of returning `NotImplemented`
assert x. __ge__ (x) is True # instead of returning `NotImplemented`

try:
    x == y
except TypeError:
    pass # instead of returning `False`
else:
    assert False

```

- **Benefits:** `<=`, `>=`, ` __le__ ` and ` __ge__ ` return the only possible result for identical objects, and `==` does not return an arbitrary result for non-identical objects (and consequently the user is fully in control of the behaviour of `==` by overriding ` __eq__ ` since `==` raises a `TypeError` instead of returning an arbitrary fallback when both ` __eq__ ` return `NotImplemented`).
- **Drawbacks:** [to be determined].
