28.5 Comparison: __eq__, __ne__, __lt__, __le__, __gt__, __ge__
In Python, comparison operators (==, !=, <, <=, >, >=) are not hardcoded behaviors of the language itself. Instead, they are syntactic sugar that trigger the invocation of specific special methods, often called “rich comparison methods,” which form a core part of the Python Data Model. This design allows developers to define how objects of their custom classes should be compared, imbuing them with intuitive, expected behavior. The six methods are __eq__ (equal, ==), __ne__ (not equal, !=), __lt__ (less than, <), __le__ (less than or equal, <=), __gt__ (greater than, >), and __ge__ (greater than or equal, >=).
The Default Behavior and the Need for Customization
By default, a custom class instance’s comparison (using == or !=) is based on object identity. It checks if two variables reference the exact same object in memory, much like the is operator. This is rarely the desired semantic meaning for data-centric objects.
class Book:
def __init__(self, title, author):
self.title = title
self.author = author
book1 = Book("Dune", "Frank Herbert")
book2 = Book("Dune", "Frank Herbert")
book3 = book1
print(book1 == book2) # Output: False (Different objects in memory)
print(book1 == book3) # Output: True (Same object)
print(book1 is book2) # Output: False
To define logical equality (e.g., two Book objects are equal if their title and author match), we must explicitly define the __eq__ method.
Implementing __eq__ and __ne__
The __eq__ method should return True if the objects are logically equal, False if they are not. It should also return NotImplemented if the comparison with the given type is not supported, which allows the other object’s __eq__ method a chance to handle the operation.
class Book:
def __init__(self, title, author):
self.title = title
self.author = author
def __eq__(self, other):
# Check if 'other' is the same type; allows for more robust comparisons
if not isinstance(other, Book):
return NotImplemented
return (self.title == other.title) and (self.author == other.author)
# __ne__ is automatically provided if __eq__ is defined but not __ne__.
# The default behavior is to return the negation of __eq__.
# Explicitly defining it is generally unnecessary unless you need different behavior.
book1 = Book("Dune", "Frank Herbert")
book2 = Book("Dune", "Frank Herbert")
book3 = Book("1984", "George Orwell")
print(book1 == book2) # Output: True (Logical equality now)
print(book1 != book3) # Output: True (Thanks to automatic __ne__)
A critical best practice is to handle type checking gracefully. Returning NotImplemented instead of False when the types are incompatible is crucial. This enables the interpreter to try the reflected operation (other.__eq__(self)) if it exists, which is essential for enabling comparisons with subclasses or other compatible types.
Implementing Ordering Methods (__lt__, __le__, __gt__, __ge__)
To make objects orderable (e.g., for use with sorted(), list.sort(), min(), max()), you must define at least one ordering operation, typically __lt__. However, the functools.total_ordering class decorator is a powerful tool that can minimize boilerplate code.
from functools import total_ordering
@total_ordering
class Book:
def __init__(self, title, author, year):
self.title = title
self.author = author
self.year = year
def __eq__(self, other):
if not isinstance(other, Book):
return NotImplemented
return self.year == other.year
def __lt__(self, other):
if not isinstance(other, Book):
return NotImplemented
return self.year < other.year
# With just __eq__ and __lt__, total_ordering provides:
# __le__ as (self < other) or (self == other)
# __gt__ as not (self < other) and not (self == other) -> which is (self > other) or (self == other) negated?
# __ge__ as not (self < other) -> which is (self > other) or (self == other)
# The exact derivation is more nuanced but the decorator handles it correctly.
books = [
Book("The Left Hand of Darkness", "Ursula K. Le Guin", 1969),
Book("Dune", "Frank Herbert", 1965),
Book("Neuromancer", "William Gibson", 1984)
]
sorted_books = sorted(books) # Sorts by year ascending
for book in sorted_books:
print(f"{book.year}: {book.title}")
Output:
1965: Dune
1969: The Left Hand of Darkness
1984: Neuromancer
Common Pitfalls and Best Practices
Return
NotImplemented, NotFalse: As emphasized, always returnNotImplementedfor unsupported types, notFalse. This is a common pitfall that breaks the symmetry of operations.return Falsewould be a definitive answer, preventing the other object’s method from being tried.Ensure Hash Consistency: If you define
__eq__and plan to use your objects in hashable collections (likesets or asdictkeys), you must also define__hash__. The rule is simple: objects that compare as equal must have the same hash value. If__eq__is defined but__hash__is not, the object becomes unhashable (its default__hash__is removed) to prevent you from accidentally creating bugs.class Book: def __init__(self, title): self.title = title def __eq__(self, other): ... # as before def __hash__(self): return hash((self.title, self.author)) # Hash the same attributes used in __eq__Beware of Inheritance with
total_ordering: The@total_orderingdecorator derives the other methods based on the ones you provide. If your logic is complex or has edge cases, it’s possible for the derived methods to have subtle bugs. For maximum control and clarity, especially in complex class hierarchies, explicitly defining all six methods might be preferable.Maintain Semantic Meaning: Your comparison logic should be intuitive. Comparing
Bookobjects by their publication year is sensible. Comparing them by the number of pages might also be sensible, but you must choose one primary meaning for ordering. Comparing them by theirauthorattribute alone would likely be confusing and break the expected behavior of the comparison operators.