Python Operator Overloading: Comparison Operators

In Python, comparison operators like ==, !=, <, >, <=, and >= don’t just work with numbers—they can also be taught to work with your own custom classes. This is called comparison operator overloading.

Instead of writing something like book1.compare(book2), you can simply write book1 == book2 or book1 < book2. This makes your code cleaner, more readable, and more natural.

Let’s say you have a Book class:

book1 = Book("Python Magic", "Lucia", 29.99)
book2 = Book("Python Magic", "Lucia", 29.99)

print(book1 == book2)  # Would you expect True or False?

Without overloading, Python compares objects by memory location, so this returns False. But with a little help from operator overloading, we can make sure it returns True when books have the same title, author, and price.

What Are Comparison Operators in Python?

Python provides six core comparison operators that let you compare values or objects. These operators and their corresponding magic methods are:

OperatorDescriptionMagic Method
==Equal to__eq__
!=Not equal to__ne__
<Less than__lt__
<=Less than or equal to__le__
>Greater than__gt__
>=Greater than or equal to__ge__

When you write an expression like a == b, Python internally calls a.__eq__(b). Similarly, a < b calls a.__lt__(b), and this applies to the other comparison operators as well.

If these magic methods are not defined in your class, Python compares objects by their identity (memory location), meaning two objects with the same data might still be treated as different. In some cases, missing these methods can lead to errors or unexpected results.

Defining these methods allows your custom objects to behave intuitively when compared, making code easier to read and more natural to use.

The __eq__ Method for ==

The __eq__ method defines equality between two objects using the == operator. For example, in a Book class, two books are equal if they have the same title and author.

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, self.author) == (other.title, other.author)

# Example usage
book1 = Book("1984", "George Orwell")
book2 = Book("1984", "George Orwell")
book3 = Book("Brave New World", "Aldous Huxley")

print(book1 == book2)  # True
print(book1 == book3)  # False

In this example, book1 and book2 are considered equal because their titles and authors match exactly. The isinstance check ensures that comparison only happens between Book objects, returning NotImplemented otherwise, which lets Python handle incompatible comparisons gracefully. This makes equality checks intuitive and meaningful for custom classes.

The __ne__ Method for !=

Although Python will use the inverse of __eq__ for != by default, it’s good practice to define __ne__ explicitly for clarity and control. This method determines if two objects are not equal.

Here’s how you might add it to the Book class to detect if two books are different:

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, self.author) == (other.title, other.author)

    def __ne__(self, other):
        return not self == other

# Example usage
book1 = Book("1984", "George Orwell")
book2 = Book("Animal Farm", "George Orwell")

print(book1 != book2)  # True
print(book1 != book1)  # False

This example shows that book1 and book2 are not the same book, so != returns True. Defining __ne__ like this ensures your class’s inequality logic is clear and consistent.

The __lt__ Method for <

The __lt__ method allows you to define what it means for one object to be “less than” another using the < operator. This is useful when your objects have measurable attributes, like numbers or dates.

Let’s say we want to compare Book objects by their price:

class Book:
    def __init__(self, title, price):
        self.title = title
        self.price = price

    def __lt__(self, other):
        if not isinstance(other, Book):
            return NotImplemented
        return self.price < other.price

# Example usage
book1 = Book("Brave New World", 9.99)
book2 = Book("The Catcher in the Rye", 12.99)

print(book1 < book2)  # True
print(book2 < book1)  # False

In this example, book1 is considered “less than” book2 because its price is lower. The __lt__ method gives your custom objects a natural way to be ordered using <.

The __le__ Method for <=

The __le__ method defines the behavior of the “less than or equal to” operator (<=). This is useful when you want your objects to support not just strict ordering (<) but also inclusive comparisons.

Continuing with the Book class from before, we can add the __le__ method to check if a book is priced lower than or equal to another:

class Book:
    def __init__(self, title, price):
        self.title = title
        self.price = price

    def __le__(self, other):
        if not isinstance(other, Book):
            return NotImplemented
        return self.price <= other.price

# Example usage
book1 = Book("1984", 8.50)
book2 = Book("Animal Farm", 8.50)
book3 = Book("Fahrenheit 451", 10.00)

print(book1 <= book2)  # True
print(book1 <= book3)  # True
print(book3 <= book1)  # False

This makes your class behave more intuitively with <=, allowing it to be used naturally in conditions or sorting logic.

The __gt__ Method for >

The __gt__ method handles the “greater than” operator (>), letting you compare objects based on criteria you define. In a real-world case like comparing books, this could be based on price, rating, or number of pages.

Here’s how you can use it to compare books by price:

class Book:
    def __init__(self, title, price):
        self.title = title
        self.price = price

    def __gt__(self, other):
        if not isinstance(other, Book):
            return NotImplemented
        return self.price > other.price

# Example usage
book1 = Book("Clean Code", 45.00)
book2 = Book("The Pragmatic Programmer", 39.99)

print(book1 > book2)  # True
print(book2 > book1)  # False

By defining __gt__, your custom class can participate in comparisons like book1 > book2, which is especially handy when sorting or filtering items based on value.

The __ge__ Method for >=

The __ge__ method supports the “greater than or equal to” operator (>=). It’s useful when you want to check if one object meets or exceeds another in value. A real-world example might be comparing books or movies to see if one is rated just as highly—or higher.

Let’s use a Book class and compare books by price again:

class Book:
    def __init__(self, title, price):
        self.title = title
        self.price = price

    def __ge__(self, other):
        if not isinstance(other, Book):
            return NotImplemented
        return self.price >= other.price

# Example usage
book1 = Book("Design Patterns", 50.00)
book2 = Book("Refactoring", 45.00)

print(book1 >= book2)  # True
print(book2 >= book1)  # False

By defining __ge__, your class behaves naturally with expressions like book1 >= book2, letting you write comparisons in a way that’s readable and expressive.

Real-World Example: Full Product Class with All Comparisons

In real life, we often sort or compare items like products by price. Let’s create a Product class that supports all six comparison operators. The comparisons will be based on price, but each product will also have a name for clarity.

This way, you can sort products, check for equality, or filter based on price—all using natural Python syntax.

class Product:
    def __init__(self, name, price):
        self.name = name
        self.price = price

    def __eq__(self, other):
        if not isinstance(other, Product):
            return NotImplemented
        return self.price == other.price

    def __ne__(self, other):
        if not isinstance(other, Product):
            return NotImplemented
        return self.price != other.price

    def __lt__(self, other):
        if not isinstance(other, Product):
            return NotImplemented
        return self.price < other.price

    def __le__(self, other):
        if not isinstance(other, Product):
            return NotImplemented
        return self.price <= other.price

    def __gt__(self, other):
        if not isinstance(other, Product):
            return NotImplemented
        return self.price > other.price

    def __ge__(self, other):
        if not isinstance(other, Product):
            return NotImplemented
        return self.price >= other.price

    def __repr__(self):
        return f"{self.name} (${self.price})"

# Example usage
p1 = Product("Laptop", 999.99)
p2 = Product("Tablet", 499.99)
p3 = Product("Smartphone", 799.99)

# Sorting products
products = [p1, p2, p3]
sorted_products = sorted(products)
print(sorted_products)

# Filtering
affordable = [p for p in products if p <= Product("", 800)]
print(affordable)

# Comparing directly
print(p1 > p3)   # True
print(p2 == p2)  # True

This full example makes your Product class behave like a built-in number type—easy to use with comparisons, sorting, and filtering, but still rich with domain meaning.

Tips for Clean Comparison Overloading

When you define comparison methods in your custom classes, a few practical patterns can make your code more readable, correct, and easier to scale. Below are smart techniques with examples to guide you.

Use Tuples for Multi-Field Comparisons

If you want to compare objects using more than one attribute (like both price and name), using tuples makes this clean and reliable.

class Product:
    def __init__(self, name, price):
        self.name = name
        self.price = price

    def __eq__(self, other):
        return (self.price, self.name) == (other.price, other.name)

    def __lt__(self, other):
        return (self.price, self.name) < (other.price, other.name)

Python compares tuples element by element. This helps you avoid writing multiple if statements for each field.

Return NotImplemented for Wrong Types

If the object being compared isn’t the right type, let Python handle it by returning NotImplemented. This avoids crashes and allows Python to try the reverse operation (like other.__eq__(self)).

def __eq__(self, other):
    if not isinstance(other, Product):
        return NotImplemented
    return (self.price, self.name) == (other.price, other.name)

Failing gracefully improves interoperability, especially when your class might be compared with something unexpected.

Keep Comparisons Symmetrical

Make sure your methods behave consistently both ways. If a == b, then b == a should also be True. Tuples help with this too by enforcing consistent comparison logic.

product1 = Product("Pen", 5)
product2 = Product("Pen", 5)

print(product1 == product2)  # True
print(product2 == product1)  # True

It makes your code predictable and avoids surprising bugs in data structures like sets or dictionaries.

Use @functools.total_ordering to Avoid Boilerplate

Instead of writing all six comparison methods, you can define just __eq__ and one ordering method (__lt__, __gt__, etc.). Then use the @total_ordering decorator to auto-fill the rest.

from functools import total_ordering

@total_ordering
class Product:
    def __init__(self, name, price):
        self.name = name
        self.price = price

    def __repr__(self):
        return f"{self.name} (${self.price})"

    def __eq__(self, other):
        if not isinstance(other, Product):
            return NotImplemented
        return (self.price, self.name) == (other.price, other.name)

    def __lt__(self, other):
        if not isinstance(other, Product):
            return NotImplemented
        return (self.price, self.name) < (other.price, other.name)

# Example use:
p1 = Product("Pencil", 2)
p2 = Product("Notebook", 3)
p3 = Product("Notebook", 3)

print(p1 < p2)      # True
print(p2 >= p3)     # True
print(p1 != p3)     # True

It saves you from writing repetitive comparison methods and keeps your code clean.

These comparison tips are small changes that make a big difference. Use tuples for clarity, return NotImplemented for safety, and reach for @total_ordering when you’re tired of writing the same logic over and over. Your classes will behave more like Python’s built-in types—and that’s always a win.

Conclusion

Comparison operator overloading lets you bring your custom classes to life. By defining methods like __eq__, __lt__, and __ge__, you give your objects the ability to behave naturally in comparisons—just like numbers, strings, or dates.

Each method serves a unique role:

OperatorMethodPurpose
==__eq__Check if two objects are equal
!=__ne__Check if two objects are not equal
<__lt__Check if one object is less than
<=__le__Check if less than or equal to
>__gt__Check if greater than
>=__ge__Check if greater than or equal to

With these tools, you can sort, filter, and compare your objects in a way that feels natural and expressive.

As a next step, try implementing these operators in your own projects—whether you’re working on books, users, accounts, or products. Let your objects speak for themselves.