Comparison Operators¶
Comparison dunder methods enable custom comparison logic and sorting for your objects.
Mental Model
Comparison dunders teach Python what "equal", "less than", and "greater than" mean for your type. Define __eq__ and __lt__, then apply @functools.total_ordering to get all six operators for free. Always return NotImplemented for incompatible types so Python can try the other operand's method instead of crashing.
Basic Comparison Methods¶
Equality: __eq__¶
```python class Point: 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
p1 = Point(1, 2) p2 = Point(1, 2) p3 = Point(3, 4)
print(p1 == p2) # True print(p1 == p3) # False print(p1 == "not a point") # False (NotImplemented → False) ```
All Comparison Methods¶
| Method | Operator | Example |
|---|---|---|
__eq__ |
== |
a == b |
__ne__ |
!= |
a != b |
__lt__ |
< |
a < b |
__le__ |
<= |
a <= b |
__gt__ |
> |
a > b |
__ge__ |
>= |
a >= b |
Complete Implementation¶
```python class Version: def init(self, major, minor, patch): self.major = major self.minor = minor self.patch = patch
def _to_tuple(self):
return (self.major, self.minor, self.patch)
def __eq__(self, other):
if not isinstance(other, Version):
return NotImplemented
return self._to_tuple() == other._to_tuple()
# __ne__ is NOT needed — Python automatically derives it from __eq__
def __lt__(self, other):
if not isinstance(other, Version):
return NotImplemented
return self._to_tuple() < other._to_tuple()
def __le__(self, other):
if not isinstance(other, Version):
return NotImplemented
return self._to_tuple() <= other._to_tuple()
def __gt__(self, other):
if not isinstance(other, Version):
return NotImplemented
return self._to_tuple() > other._to_tuple()
def __ge__(self, other):
if not isinstance(other, Version):
return NotImplemented
return self._to_tuple() >= other._to_tuple()
def __repr__(self):
return f"Version({self.major}, {self.minor}, {self.patch})"
v1 = Version(1, 0, 0) v2 = Version(2, 0, 0) v3 = Version(1, 5, 0)
print(v1 < v2) # True print(v1 < v3) # True print(v2 > v3) # True print(sorted([v2, v1, v3])) # [Version(1, 0, 0), Version(1, 5, 0), Version(2, 0, 0)] ```
Using @total_ordering¶
The functools.total_ordering decorator reduces boilerplate by deriving missing comparison methods from __eq__ and one ordering method.
```python 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
def __repr__(self):
return f"Student({self.name!r}, {self.grade})"
All comparisons work!¶
alice = Student("Alice", 90) bob = Student("Bob", 85) charlie = Student("Charlie", 90)
print(alice > bob) # True (derived from lt) print(alice >= bob) # True (derived from lt and eq) print(alice <= bob) # False (derived from lt and eq) print(alice == charlie) # True print(alice != bob) # True (derived from eq) ```
How @total_ordering Works¶
You provide __eq__ + one of (__lt__, __le__, __gt__, __ge__), and it derives the rest:
| You Implement | Decorator Derives |
|---|---|
__eq__ + __lt__ |
__le__, __gt__, __ge__ |
__eq__ + __le__ |
__lt__, __gt__, __ge__ |
__eq__ + __gt__ |
__lt__, __le__, __ge__ |
__eq__ + __ge__ |
__lt__, __le__, __gt__ |
Performance Note¶
```python
@total_ordering has slight performance overhead¶
For performance-critical code, implement all methods manually¶
Or use slots with manual implementation¶
class FastPoint: slots = ('x', 'y')
def __init__(self, x, y):
self.x = x
self.y = y
def __eq__(self, other):
return self.x == other.x and self.y == other.y
def __lt__(self, other):
return (self.x, self.y) < (other.x, other.y)
def __le__(self, other):
return (self.x, self.y) <= (other.x, other.y)
def __gt__(self, other):
return (self.x, self.y) > (other.x, other.y)
def __ge__(self, other):
return (self.x, self.y) >= (other.x, other.y)
```
Practical Advice
In practice, use @total_ordering unless you have a specific reason not to. Manual implementation of all six comparison methods is rarely needed — the two main exceptions are: (1) performance-critical code where the decorator's overhead matters (it wraps each derived method in an extra function call), and (2) non-standard comparison semantics where the derived methods would produce incorrect results. For the vast majority of classes, __eq__ + __lt__ + @total_ordering is the right choice.
Hashing and Equality¶
Objects that compare equal must have equal hashes. Defining __eq__ without __hash__ is one of the most common dunder-method mistakes --- Python will set __hash__ to None, making your objects unhashable (they cannot be used in sets or as dictionary keys). Always pair the two.
```python class Color: def init(self, r, g, b): self.r = r self.g = g self.b = b
def __eq__(self, other):
if not isinstance(other, Color):
return NotImplemented
return (self.r, self.g, self.b) == (other.r, other.g, other.b)
def __hash__(self):
return hash((self.r, self.g, self.b))
def __repr__(self):
return f"Color({self.r}, {self.g}, {self.b})"
Now Color can be used in sets and as dict keys¶
red = Color(255, 0, 0) also_red = Color(255, 0, 0)
colors = {red, also_red} print(len(colors)) # 1 (same hash and equal)
color_names = {red: "red", Color(0, 255, 0): "green"} print(color_names[also_red]) # "red" ```
Hash Rules¶
```python
Rule 1: Equal objects must have equal hashes¶
a == b → hash(a) == hash(b)
Rule 2: Unequal objects CAN have equal hashes (collisions are OK)¶
hash(a) == hash(b) ↛ a == b
Rule 3: Mutable objects should not be hashable¶
(their hash could change, breaking dict/set invariants)¶
```
Making Objects Unhashable¶
```python class MutablePoint: def init(self, x, y): self.x = x self.y = y
def __eq__(self, other):
if not isinstance(other, MutablePoint):
return NotImplemented
return self.x == other.x and self.y == other.y
# Explicitly make unhashable
__hash__ = None
p = MutablePoint(1, 2)
hash(p) # TypeError: unhashable type: 'MutablePoint'¶
{p} # TypeError: unhashable type: 'MutablePoint'¶
```
Identity vs Equality¶
```python class Box: def init(self, value): self.value = value
def __eq__(self, other):
if not isinstance(other, Box):
return NotImplemented
return self.value == other.value
a = Box(42) b = Box(42) c = a
Equality (==) uses eq¶
print(a == b) # True (same value) print(a == c) # True (same value)
Identity (is) compares memory addresses¶
print(a is b) # False (different objects) print(a is c) # True (same object) ```
Comparing with None¶
```python class OptionalValue: def init(self, value=None): self.value = value
def __eq__(self, other):
# Handle None comparison
if other is None:
return self.value is None
if not isinstance(other, OptionalValue):
return NotImplemented
return self.value == other.value
empty = OptionalValue() filled = OptionalValue(42)
print(empty == None) # True print(filled == None) # False ```
Comparison with Different Types¶
```python class Temperature: def init(self, celsius): self.celsius = celsius
def __eq__(self, other):
if isinstance(other, Temperature):
return self.celsius == other.celsius
if isinstance(other, (int, float)):
return self.celsius == other
return NotImplemented
def __lt__(self, other):
if isinstance(other, Temperature):
return self.celsius < other.celsius
if isinstance(other, (int, float)):
return self.celsius < other
return NotImplemented
t = Temperature(20) print(t == 20) # True print(t == Temperature(20)) # True print(t < 25) # True print(t > Temperature(15)) # True ```
Chained Comparisons¶
Python's chained comparisons (like a < b < c) work automatically:
```python from functools import total_ordering
@total_ordering class Score: def init(self, value): self.value = value
def __eq__(self, other):
if not isinstance(other, Score):
return NotImplemented
return self.value == other.value
def __lt__(self, other):
if not isinstance(other, Score):
return NotImplemented
return self.value < other.value
low = Score(10) mid = Score(50) high = Score(90)
Chained comparison¶
print(low < mid < high) # True
Equivalent to: low < mid and mid < high¶
```
Practical Example: Priority Queue Item¶
```python from functools import total_ordering
@total_ordering class PriorityItem: """Item for priority queue with (priority, data) comparison."""
_counter = 0 # Tie-breaker for equal priorities
def __init__(self, priority, data):
self.priority = priority
self.data = data
PriorityItem._counter += 1
self._order = PriorityItem._counter
def __eq__(self, other):
if not isinstance(other, PriorityItem):
return NotImplemented
return (self.priority, self._order) == (other.priority, other._order)
def __lt__(self, other):
if not isinstance(other, PriorityItem):
return NotImplemented
# Lower priority number = higher priority
# Earlier insertion = higher priority (for ties)
return (self.priority, self._order) < (other.priority, other._order)
def __repr__(self):
return f"PriorityItem({self.priority}, {self.data!r})"
import heapq
tasks = [] heapq.heappush(tasks, PriorityItem(2, "low priority")) heapq.heappush(tasks, PriorityItem(1, "high priority")) heapq.heappush(tasks, PriorityItem(1, "also high priority"))
while tasks: item = heapq.heappop(tasks) print(item)
Output:¶
PriorityItem(1, 'high priority')¶
PriorityItem(1, 'also high priority')¶
PriorityItem(2, 'low priority')¶
```
Sorting Custom Objects¶
```python from functools import total_ordering
@total_ordering class Employee: def init(self, name, salary, years): self.name = name self.salary = salary self.years = years
def __eq__(self, other):
if not isinstance(other, Employee):
return NotImplemented
return (self.salary, self.years) == (other.salary, other.years)
def __lt__(self, other):
if not isinstance(other, Employee):
return NotImplemented
# Sort by salary (desc), then years (desc)
return (-self.salary, -self.years) < (-other.salary, -other.years)
def __repr__(self):
return f"Employee({self.name!r}, ${self.salary}, {self.years}y)"
employees = [ Employee("Alice", 75000, 5), Employee("Bob", 80000, 3), Employee("Charlie", 75000, 8), ]
Using default sort (uses lt)¶
for emp in sorted(employees): print(emp)
Output:¶
Employee('Bob', $80000, 3y)¶
Employee('Charlie', $75000, 8y)¶
Employee('Alice', $75000, 5y)¶
```
Key Takeaways¶
- Implement
__eq__for equality; Python provides__ne__automatically - Use
@total_orderingto avoid boilerplate—just implement__eq__and one of__lt__,__le__,__gt__,__ge__ - Return
NotImplemented(notFalse) for unsupported types - If you define
__eq__, also define__hash__for hashable objects - Set
__hash__ = Nonefor mutable objects that shouldn't be hashable - Tuple comparison is a clean way to implement multi-field comparisons
- Identity (
is) and equality (==) are different concepts
Runnable Example: comparison_operators_tutorial.py¶
```python """ Example 2: Comparison Operators Demonstrates: eq, ne, lt, le, gt, ge """
class Student: """A student with grade-based comparisons."""
def __init__(self, name, grade):
self.name = name
self.grade = grade
def __repr__(self):
return f"Student('{self.name}', {self.grade})"
def __eq__(self, other):
"""Check if two students have the same grade."""
if not isinstance(other, Student):
return NotImplemented
return self.grade == other.grade
def __ne__(self, other):
"""Check if two students have different grades."""
if not isinstance(other, Student):
return NotImplemented
return self.grade != other.grade
def __lt__(self, other):
"""Check if this student's grade is less than another's."""
if not isinstance(other, Student):
return NotImplemented
return self.grade < other.grade
def __le__(self, other):
"""Check if this student's grade is less than or equal to another's."""
if not isinstance(other, Student):
return NotImplemented
return self.grade <= other.grade
def __gt__(self, other):
"""Check if this student's grade is greater than another's."""
if not isinstance(other, Student):
return NotImplemented
return self.grade > other.grade
def __ge__(self, other):
"""Check if this student's grade is greater than or equal to another's."""
if not isinstance(other, Student):
return NotImplemented
return self.grade >= other.grade
class Version: """A version class that can be compared (e.g., 1.2.3)."""
def __init__(self, major, minor, patch):
self.major = major
self.minor = minor
self.patch = patch
def __repr__(self):
return f"Version({self.major}, {self.minor}, {self.patch})"
def __str__(self):
return f"{self.major}.{self.minor}.{self.patch}"
def _as_tuple(self):
"""Convert to tuple for easy comparison."""
return (self.major, self.minor, self.patch)
def __eq__(self, other):
if not isinstance(other, Version):
return NotImplemented
return self._as_tuple() == other._as_tuple()
def __lt__(self, other):
if not isinstance(other, Version):
return NotImplemented
return self._as_tuple() < other._as_tuple()
def __le__(self, other):
if not isinstance(other, Version):
return NotImplemented
return self._as_tuple() <= other._as_tuple()
def __gt__(self, other):
if not isinstance(other, Version):
return NotImplemented
return self._as_tuple() > other._as_tuple()
def __ge__(self, other):
if not isinstance(other, Version):
return NotImplemented
return self._as_tuple() >= other._as_tuple()
Examples¶
if name == "main":
# ============================================================================
print("=== Student Comparison Examples ===")
alice = Student("Alice", 85)
bob = Student("Bob", 92)
charlie = Student("Charlie", 85)
print(f"{alice.name}: {alice.grade}")
print(f"{bob.name}: {bob.grade}")
print(f"{charlie.name}: {charlie.grade}")
print(f"\nAlice == Charlie: {alice == charlie}")
print(f"Alice == Bob: {alice == bob}")
print(f"Alice < Bob: {alice < bob}")
print(f"Bob > Alice: {bob > alice}")
print(f"Alice <= Charlie: {alice <= charlie}")
# Sorting students by grade
students = [bob, alice, charlie, Student("David", 78), Student("Eve", 95)]
print("\n=== Sorting Students ===")
print("Original:", students)
sorted_students = sorted(students)
print("Sorted by grade:", sorted_students)
print("\n\n=== Version Comparison Examples ===")
v1 = Version(1, 2, 3)
v2 = Version(1, 2, 4)
v3 = Version(2, 0, 0)
v4 = Version(1, 2, 3)
print(f"v1: {v1}")
print(f"v2: {v2}")
print(f"v3: {v3}")
print(f"v4: {v4}")
print(f"\nv1 == v4: {v1 == v4}")
print(f"v1 < v2: {v1 < v2}")
print(f"v2 < v3: {v2 < v3}")
print(f"v3 > v1: {v3 > v1}")
versions = [v3, v1, v2, Version(0, 9, 5)]
print("\n=== Sorting Versions ===")
print("Original:", versions)
print("Sorted:", sorted(versions))
```
Runnable Example: hash_eq_slots_example.py¶
```python """ Magic Methods: hash, eq, and slots
When using custom objects in sets or as dict keys, Python needs: - hash() to compute a hash code (for bucket placement) - eq() to check if two objects are "equal"
Rule: Objects that compare equal MUST have the same hash. Same hash does NOT mean equal (hash collisions exist).
Topics covered: - hash and eq for hashable objects - slots for memory-efficient classes - setitem and getitem for container-like classes
Based on concepts from Python-100-Days example16 and ch06/dunder materials. """
=============================================================================¶
Example 1: Making Objects Hashable¶
=============================================================================¶
class Student: """A student that can be used in sets and as dict keys.
__slots__ restricts attributes to save memory.
__hash__ and __eq__ make instances hashable.
>>> s1 = Student(1001, 'Alice')
>>> s2 = Student(1001, 'Alice')
>>> s1 == s2
True
>>> hash(s1) == hash(s2)
True
>>> s1 is s2 # Different objects
False
"""
__slots__ = ('student_id', 'name', 'grade')
def __init__(self, student_id: int, name: str):
self.student_id = student_id
self.name = name
def __hash__(self) -> int:
"""Hash based on student_id and name.
Must be consistent with __eq__: if a == b, then hash(a) == hash(b).
Using tuple hashing is a clean pattern.
"""
return hash((self.student_id, self.name))
def __eq__(self, other) -> bool:
"""Two students are equal if they have the same ID and name."""
if not isinstance(other, Student):
return NotImplemented
return (self.student_id == other.student_id and
self.name == other.name)
def __str__(self):
return f'{self.student_id}: {self.name}'
def __repr__(self):
return f'Student({self.student_id}, {self.name!r})'
def demo_hashable(): """Demonstrate hashable objects in sets and dicts.""" print("=== Hashable Objects in Sets ===")
students = set()
students.add(Student(1001, 'Alice'))
students.add(Student(1001, 'Alice')) # Duplicate - won't be added
students.add(Student(1002, 'Bob'))
print(f"Set size: {len(students)} (added 3, but 2 unique)")
print(f"Students: {students}")
print()
=============================================================================¶
Example 2: slots for Memory Efficiency¶
=============================================================================¶
def demo_slots(): """Demonstrate slots behavior.""" print("=== slots Behavior ===")
stu = Student(1234, 'Charlie')
stu.grade = 'A' # OK - 'grade' is in __slots__
print(f"Student: {stu}, Grade: {stu.grade}")
try:
stu.email = 'charlie@example.com' # Error - not in __slots__
except AttributeError as e:
print(f"AttributeError: {e}")
print(f"Allowed attributes: {Student.__slots__}")
print(f"Has __dict__: {hasattr(stu, '__dict__')}") # False with __slots__
print()
=============================================================================¶
Example 3: Container-Like Class with setitem and getitem¶
=============================================================================¶
class Registry: """A registry that stores items by key using dict-like syntax.
Implements __setitem__ and __getitem__ so you can use
bracket notation: registry[key] = value
"""
def __init__(self, name: str):
self.name = name
self._items: dict = {}
def __setitem__(self, key, value):
"""Enable registry[key] = value syntax."""
self._items[key] = value
def __getitem__(self, key):
"""Enable registry[key] syntax."""
return self._items[key]
def __contains__(self, key) -> bool:
"""Enable 'key in registry' syntax."""
return key in self._items
def __len__(self) -> int:
return len(self._items)
def __repr__(self):
return f"Registry('{self.name}', {len(self)} items)"
def demo_container(): """Demonstrate container-like class usage.""" print("=== Container Protocol (setitem, getitem) ===")
school = Registry('Python Academy')
school[1001] = Student(1001, 'Alice')
school[1002] = Student(1002, 'Bob')
school[1003] = Student(1003, 'Charlie')
print(f"Registry: {school}")
print(f"school[1002] = {school[1002]}")
print(f"1003 in school: {1003 in school}")
print(f"9999 in school: {9999 in school}")
print()
=============================================================================¶
Example 4: Why Both hash and eq Matter¶
=============================================================================¶
def demo_hash_eq_relationship(): """Explain the relationship between hash and eq.""" print("=== Hash and Equality Relationship ===")
s1 = Student(1001, 'Alice')
s2 = Student(1001, 'Alice')
s3 = Student(1001, 'Bob')
print(f"s1 = {s1!r}")
print(f"s2 = {s2!r}")
print(f"s3 = {s3!r}")
print()
print(f"s1 == s2: {s1 == s2} (same id and name)")
print(f"s1 is s2: {s1 is s2} (different objects)")
print(f"hash(s1) == hash(s2): {hash(s1) == hash(s2)} (equal -> same hash)")
print()
print(f"s1 == s3: {s1 == s3} (different name)")
print(f"hash(s1) == hash(s3): {hash(s1) == hash(s3)} (may differ)")
print()
print("Rules:")
print(" 1. Equal objects MUST have the same hash")
print(" 2. Same hash does NOT mean equal (collisions)")
print(" 3. If you define __eq__, define __hash__ too")
print(" 4. Mutable objects generally should NOT be hashable")
=============================================================================¶
Main¶
=============================================================================¶
if name == 'main': demo_hashable() demo_slots() demo_container() demo_hash_eq_relationship() ```
Exercises¶
Exercise 1.
Create a Version class with major, minor, and patch fields. Implement __eq__, __lt__, and use functools.total_ordering to get the remaining comparison operators. Show that versions can be sorted: Version(2, 0, 0) > Version(1, 9, 9).
Solution to Exercise 1
from functools import total_ordering
@total_ordering
class Version:
def __init__(self, major, minor, patch):
self.major = major
self.minor = minor
self.patch = patch
def __eq__(self, other):
return (self.major, self.minor, self.patch) == (other.major, other.minor, other.patch)
def __lt__(self, other):
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})"
versions = [Version(2, 0, 0), Version(1, 9, 9), Version(1, 10, 0), Version(2, 0, 1)]
print(sorted(versions))
# [Version(1, 9, 9), Version(1, 10, 0), Version(2, 0, 0), Version(2, 0, 1)]
print(Version(2, 0, 0) > Version(1, 9, 9)) # True
Exercise 2.
Write a Student class with name and gpa. Implement __eq__ (comparing by name and gpa) and __hash__ (so students can be stored in sets). Also implement __lt__ based on gpa for sorting. Show students in a sorted list and a set with duplicates removed.
Solution to Exercise 2
class Student:
def __init__(self, name, gpa):
self.name = name
self.gpa = gpa
def __eq__(self, other):
return self.name == other.name and self.gpa == other.gpa
def __hash__(self):
return hash((self.name, self.gpa))
def __lt__(self, other):
return self.gpa < other.gpa
def __repr__(self):
return f"Student('{self.name}', {self.gpa})"
students = [Student("Alice", 3.8), Student("Bob", 3.5),
Student("Alice", 3.8), Student("Charlie", 3.9)]
print(sorted(students))
unique = set(students)
print(len(unique)) # 3 — duplicate Alice removed
Exercise 3.
Build a Temperature class with a value and scale ("C" or "F"). Implement comparison operators that convert to a common scale before comparing. For example, Temperature(32, "F") == Temperature(0, "C") should be True. Use functools.total_ordering.
Solution to Exercise 3
from functools import total_ordering
@total_ordering
class Temperature:
def __init__(self, value, scale="C"):
self.value = value
self.scale = scale
def _to_celsius(self):
if self.scale == "C":
return self.value
return (self.value - 32) * 5 / 9
def __eq__(self, other):
return abs(self._to_celsius() - other._to_celsius()) < 1e-9
def __lt__(self, other):
return self._to_celsius() < other._to_celsius()
def __repr__(self):
return f"Temperature({self.value}, '{self.scale}')"
print(Temperature(32, "F") == Temperature(0, "C")) # True
print(Temperature(212, "F") == Temperature(100, "C")) # True
print(Temperature(100, "C") > Temperature(200, "F")) # True
Exercise 4.
Predict the output of both print statements. Explain what happens when Python evaluates a > b given that Student defines __ge__ but not __gt__.
```python class Student: def init(self, name): self.name = name
def __ge__(self, other):
if other.name == "Lee":
return True
else:
return False
a = Student("Kim") b = Student("Lee") print(a >= b) print(a > b) ```
Solution to Exercise 4
class Student:
def __init__(self, name):
self.name = name
def __ge__(self, other):
if other.name == "Lee":
return True
else:
return False
a = Student("Kim")
b = Student("Lee")
print(a >= b) # True — calls a.__ge__(b), other.name == "Lee" → True
print(a > b) # TypeError: '>' not supported between instances of 'Student' and 'Student'
# Explanation:
# a >= b calls a.__ge__(b), which is defined and returns True.
#
# a > b tries a.__gt__(b) first — not defined, returns NotImplemented.
# Then Python tries the reflected operation: b.__lt__(a) — also not
# defined, returns NotImplemented. Both sides fail → TypeError.
#
# Key lesson: defining __ge__ does NOT automatically give you __gt__.
# Python does not derive one comparison from another unless you use
# @functools.total_ordering (which derives the missing operators from
# __eq__ + one ordering method).