A practical guide to Python's comparison methods (__eq__, __lt__, etc.) with real-world examples, best practices, and common pitfalls.
In Python, objects aren’t just data containers, they can define how they should be compared with each other. That’s what comparison methods are all about. They allow you to control the meaning of expressions like a == b
, a < b
, or a >= b
.
This post will walk you through these special methods, explain when to use them, how to use them correctly, and what pitfalls to avoid.
Python uses special (or "magic") methods to handle built-in operations. For comparisons, these methods start and end with double underscores (__
), and include:
Method | Operator it supports |
---|---|
__eq__(self, other) |
== |
__ne__(self, other) |
!= |
__lt__(self, other) |
< |
__le__(self, other) |
<= |
__gt__(self, other) |
> |
__ge__(self, other) |
>= |
These methods let you define what it means for your own class instances to be "equal", "less than", "greater than", etc.
Here are some use cases where you’d need to define comparison behavior:
You want to sort a list of Student
objects by GPA. But by default, Python has no idea how to compare them. You must define comparison logic using methods like __lt__
.
If you're putting User
objects into a set or using them as dictionary keys, Python uses __eq__
and __hash__
to check for uniqueness.
Comparing Version("1.2.3") < Version("2.0.0")
in a software updater? You’ll need to define how versions compare.
Let’s look at practical examples for each.
__eq__
class Book:
def __init__(self, title, author):
self.title = title
self.author = author
def __eq__(self, other):
if not isinstance(other, Book):
return NotImplemented
return self.title == other.title and self.author == other.author
This allows you to write:
Book("1984", "Orwell") == Book("1984", "Orwell") # True
Why return NotImplemented
?
Because if other
isn’t a Book
, Python should try the reverse comparison (other.__eq__(self)
) or raise a TypeError
.
__lt__
and @total_ordering
You don’t have to implement all six comparison methods. Python’s functools.total_ordering
helps you generate the rest if you just provide __eq__
and one of (__lt__
, __gt__
, etc.)
from functools import total_ordering
@total_ordering
class Student:
def __init__(self, name, gpa):
self.name = name
self.gpa = gpa
def __eq__(self, other):
return self.gpa == other.gpa
def __lt__(self, other):
return self.gpa < other.gpa
Now you can do:
alice = Student("Alice", 3.5)
bob = Student("Bob", 3.7)
print(alice < bob) # True
print(alice >= bob) # False
If you implement __eq__
but forget to implement __hash__
, your objects won’t behave correctly in set
or as dictionary keys.
class Broken:
def __eq__(self, other):
return True
s = {Broken(), Broken()}
print(len(s)) # Still 2, because they're unhashable
Rule of thumb: If your object is immutable and implements __eq__
, also implement __hash__
.
Be careful that your comparison logic doesn’t lead to nonsense like:
a < b == c < a # This should NEVER be True
Always make sure your logic is transitive, symmetric, and reflexive, where appropriate.
Don't assume the other
object is the same type:
def __lt__(self, other):
return self.value < other.value # Could crash if other is a string or None!
Instead:
if not isinstance(other, MyClass):
return NotImplemented
Let’s say you’re building a movie database. You want to sort movies first by rating, then by release year.
@total_ordering
class Movie:
def __init__(self, title, rating, year):
self.title = title
self.rating = rating
self.year = year
def __eq__(self, other):
return (self.rating, self.year) == (other.rating, other.year)
def __lt__(self, other):
return (self.rating, self.year) > (other.rating, other.year) # reverse logic for higher rating
movies = [
Movie("A", 8.5, 2020),
Movie("B", 8.5, 2019),
Movie("C", 7.0, 2021)
]
sorted_movies = sorted(movies)
print([m.title for m in sorted_movies]) # ['A', 'B', 'C']
Here we’re comparing tuples of attributes for concise and readable logic.
__eg__
and __hash__
: The Dynamic DuoIf you want your objects to work in sets and as dictionary keys, you must implement both __eq__
and __hash__
in a consistent way.
class User:
def __init__(self, username):
self.username = username
def __eq__(self, other):
return self.username == other.username
def __hash__(self):
return hash(self.username)
u1 = User("alice")
u2 = User("alice")
print(u1 == u2) # True
print({u1, u2}) # Just one object in the set
==
, <
, >
, <=
, etc. directly in unit tests.sorted()
.None
, different types, self-comparison.__eq__
, __lt__
, etc.__eq__
and __hash__
for equality + hashing (e.g., in sets/dicts).@total_ordering
to reduce boilerplate when defining full ordering.NotImplemented
if necessary.