Add missing default implementations of __le__ and __ge__

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

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

def __eq__(self, other):
    return self is other or NotImplemented  # reflexivity property
def __ne__(self, other):
    result = self.__eq__(other)
    if result is not NotImplemented:
        return not result
    return NotImplemented
def __lt__(self, other):
    # Current implementation: return NotImplemented
    # New implementation:
    return self is not other and NotImplemented  # irreflexivity property
def __gt__(self, other):
    # Current implementation: return NotImplemented
    # New implementation:
    return self is not other and NotImplemented  # irreflexivity property
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
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:

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:

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:

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