pyguides

__lt__ / __le__ / __gt__ / __ge__

__lt__(self, other)

Overview

Python’s rich comparison methods — __lt__, __le__, __gt__, and __ge__ — let you define how your objects behave when compared with <, <=, >, and >=. Without these methods, Python falls back to object identity, comparing whether two variables point to the same object in memory.

These methods are fundamental to making custom types behave like native Python types. They work alongside __eq__ and __ne__ to complete the full set of comparison operators.

Signature

All four methods share the same signature:

def __lt__(self, other): ...
def __le__(self, other): ...
def __gt__(self, other): ...
def __ge__(self, other): ...
  • self — the object on the left side of the operator
  • other — the object on the right side
  • Return True, False, or NotImplemented

Return Values

Return True or False when the comparison succeeds. Return NotImplemented (the singleton, not a string) when the operation is not defined for the given types. This tells Python to try the reflected method on other.

class Money:
    def __init__(self, amount):
        self.amount = amount

    def __lt__(self, other):
        if isinstance(other, Money):
            return self.amount < other.amount
        return NotImplemented  # Let Python try other.__gt__

dollar = Money(100)
print(dollar < Money(200))  # True
# output: True

Returning NotImplemented rather than raising TypeError is the convention. Raising an exception would prevent Python from attempting the reflected operation.

Basic Implementation

Implement __lt__ to make objects sortable and comparable:

class Version:
    def __init__(self, major, minor, patch):
        self.major = major
        self.minor = minor
        self.patch = patch

    def __lt__(self, other):
        if not isinstance(other, Version):
            return NotImplemented
        return (self.major, self.minor, self.patch) < (other.major, other.minor, other.patch)

    def __repr__(self):
        return f"Version({self.major}, {self.minor}, {self.patch})"

v1 = Version(1, 2, 3)
v2 = Version(1, 2, 4)
v3 = Version(2, 0, 0)

print(v1 < v2)   # True
print(v2 < v3)   # True
print(v1 >= v1)  # True
# output: True
# output: True
# output: True

Tuples compare lexicographically in Python, so comparing (self.major, self.minor, self.patch) against the same tuple from other is a clean way to implement version comparison without writing multiple if statements.

Reflection Pairs

Python handles comparison operators in pairs:

OperatorMethodReflected to
<__lt__other.__gt__
<=__le__other.__ge__
>__gt__other.__lt__
>=__ge__other.__le__
==__eq__its own reflection
!=__ne__its own reflection

When a < b evaluates, Python first calls a.__lt__(b). If that returns NotImplemented, Python calls b.__gt__(a). This reflection mechanism lets you define only __lt__ and still get working > comparisons automatically (though using @total_ordering is cleaner).

functools.total_ordering

Defining all six comparison methods manually is tedious. The @functools.total_ordering decorator (added in Python 3.2) fills in the missing methods from a minimal set:

from functools import total_ordering

@total_ordering
class Student:
    def __init__(self, name, grade):
        self.name = name
        self.grade = grade

    def __eq__(self, other):
        if not isinstance(other, Student):
            return NotImplemented
        return self.grade == other.grade

    def __lt__(self, other):
        if not isinstance(other, Student):
            return NotImplemented
        return self.grade < other.grade

alice = Student("Alice", 85)
bob = Student("Bob", 92)
charlie = Student("Charlie", 85)

print(alice < bob)        # True
print(bob > alice)        # True
print(alice == charlie)   # True
print(alice <= charlie)  # True (generated from __eq__ and __lt__)
print(bob >= charlie)    # True (generated)
# output: True
# output: True
# output: True
# output: True
# output: True

The class needs to define one of __lt__(), __le__(), __gt__(), or __ge__(), plus __eq__(). The decorator then generates the rest.

Performance trade-off

The docs explicitly warn that @total_ordering adds slower execution and more complex stack traces for the derived methods. In performance-critical code, implementing all six manually is faster:

# Faster: implement all six directly
class Point:
    __slots__ = ('x', 'y')

    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __eq__(self, other):
        if not isinstance(other, Point):
            return NotImplemented
        return self.x == other.x and self.y == other.y

    def __lt__(self, other):
        if not isinstance(other, Point):
            return NotImplemented
        return (self.x, self.y) < (other.x, other.y)

    def __le__(self, other):
        if not isinstance(other, Point):
            return NotImplemented
        return (self.x, self.y) <= (other.x, other.y)

    def __gt__(self, other):
        if not isinstance(other, Point):
            return NotImplemented
        return (self.x, self.y) > (other.x, other.y)

    def __ge__(self, other):
        if not isinstance(other, Point):
            return NotImplemented
        return (self.x, self.y) >= (other.x, other.y)

Common Gotchas

1. Do not return non-boolean values

Comparisons must return True or False. A common mistake is returning a truthy integer:

# Broken — looks like it works but causes problems
class Broken:
    def __init__(self, value):
        self.value = value

    def __lt__(self, other):
        return self.value - other.value  # Returns int, not bool

# When used in a boolean context, this breaks:
if Broken(5) < Broken(3):
    print("never reached")
# output: never reached

# The subtraction returns 2 (truthy), so the branch runs.
# But worse: sorting a list of Broken objects fails
# because Python expects bool returns from comparisons.

Always return an explicit boolean:

def __lt__(self, other):
    return self.value < other.value  # Returns bool, not int

2. Return NotImplemented, not false

If the comparison is undefined for the operand types, return NotImplemented so Python can try the reflected method. Returning False claims the comparison is unequal, which is wrong:

def __lt__(self, other):
    if not isinstance(other, Version):
        return NotImplemented  # Correct: let Python try other.__gt__
        # return False         # Wrong: claims they're unequal

3. total_ordering does not override existing methods

If a superclass already defines a comparison operator, @total_ordering will not override it. This can lead to unexpected behavior in inheritance hierarchies.

4. Mixed-type comparisons with subclasses

If other is a subclass of self’s type, Python gives priority to other’s reflected method. Otherwise, self.__lt__ is tried first. This is defined in the language specification and is usually not something you need to worry about, but it matters when designing class hierarchies with comparisons.

Sorting Objects

Defining __lt__ makes your objects sortable with Python’s built-in sorted():

class Task:
    def __init__(self, name, priority):
        self.name = name
        self.priority = priority

    def __lt__(self, other):
        return self.priority < other.priority

    def __repr__(self):
        return f"Task({self.name}, priority={self.priority})"

tasks = [
    Task("Write tests", 2),
    Task("Fix bug", 1),
    Task("Deploy", 3),
]

sorted_tasks = sorted(tasks)
print(sorted_tasks)
# output: [Task(Fix bug, priority=1), Task(Write tests, priority=2), Task(Deploy, priority=3)]

See Also

  • dunder-eq — The __eq__ method for equality comparisons; Python derives __ne__ from it automatically
  • dunder-eq-ne — Coverage of both equality and inequality dunder methods together
  • functools.total_ordering — The @total_ordering decorator that auto-generates missing comparison methods