__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 operatorother— the object on the right side- Return
True,False, orNotImplemented
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:
| Operator | Method | Reflected 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_orderingdecorator that auto-generates missing comparison methods