Skip to Content
Course content

125: Magic Methods for Comparison Operators

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've seen this happen to a lot of junior devs when they start building their own domain models. You create a class to represent a real-world concept—like a software version, a price, or a game character's stats—and then you try to sort a list of them or check if one is "greater" than another. Then, Python hits you with a TypeError.

class SoftwareVersion:
    def __init__(self, major, minor, patch):
        self.major = major
        self.minor = minor
        self.patch = patch

    def __repr__(self):
        return f"{self.major}.{self.minor}.{self.patch}"

v1 = SoftwareVersion(1, 2, 0)
v2 = SoftwareVersion(1, 10, 0)

if v1 < v2:
    print("Updating software...")
# TypeError: '<' not supported between instances of 'SoftwareVersion' and 'SoftwareVersion'

The 'TypeError' that stops your app in its tracks

The problem here is that Python has no inherent idea of what makes one SoftwareVersion "less than" another. To Python, your object is just a heap of data in memory. When you use the < operator, Python looks for a specific "magic method" inside your class. If it doesn't find it, it gives up and throws that error.

You might be tempted to just write a helper function like is_version_less(v1, v2), but that's a mistake. It makes your code clunky and prevents you from using built-in Python features like sorted() or min() and max(), which rely entirely on these magic methods.

Teaching Python how to actually compare objects

To fix this, we need to implement the comparison dunder (double-underscore) methods. The most critical ones are __eq__ (equal) and __lt__ (less than).

When comparing versions, we can't just compare the whole object; we have to compare the components in order of importance: major, then minor, then patch. A great trick here is to use tuples. Python knows how to compare tuples element-by-element, so we can leverage that instead of writing a mountain of if/else statements.

class SoftwareVersion:
    def __init__(self, major, minor, patch):
        self.major = major
        self.minor = minor
        self.patch = patch

    def __eq__(self, other):
        if not isinstance(other, SoftwareVersion):
            return NotImplemented
        return (self.major, self.minor, self.patch) == (other.major, other.minor, other.patch)

    def __lt__(self, other):
        if not isinstance(other, SoftwareVersion):
            return NotImplemented
        return (self.major, self.minor, self.patch) < (other.major, other.minor, other.patch)

    def __repr__(self):
        return f"{self.major}.{self.minor}.{self.patch}"

v1 = SoftwareVersion(1, 2, 0)
v2 = SoftwareVersion(1, 10, 0)

print(v1 < v2)  # Now it returns True!

I added that isinstance check and the NotImplemented return for a reason. If you accidentally try to compare a SoftwareVersion object to a string or an integer, returning NotImplemented tells Python, "I don't know how to handle this type," which allows Python to potentially ask the other object if it knows how to handle the comparison before finally failing.

Saving time with total_ordering

Now, you might be thinking: "Do I really have to write __gt__, __le__, and __ge__ as well?" The answer is: usually, no. Writing all six comparison methods is tedious and error-prone.

Python provides a decorator in the functools module called total_ordering. If you define __eq__ and one other comparison method (like __lt__), this decorator automatically fills in the rest for you. It's a massive time-saver.

from functools import total_ordering

@total_ordering
class SoftwareVersion:
    def __init__(self, major, minor, patch):
        self.major = major
        self.minor = minor
        self.patch = patch

    def __eq__(self, other):
        if not isinstance(other, SoftwareVersion):
            return NotImplemented
        return (self.major, self.minor, self.patch) == (other.major, other.minor, other.patch)

    def __lt__(self, other):
        if not isinstance(other, SoftwareVersion):
            return NotImplemented
        return (self.major, self.minor, self.patch) < (other.major, other.minor, other.patch)

v1 = SoftwareVersion(1, 2, 0)
v2 = SoftwareVersion(1, 10, 0)

# These now work automatically because of @total_ordering
print(v1 > v2)  # False
print(v1 <= v2) # True



📋 Practical Task

Exercise: Implementing a Product Ranking System

You are building an e-commerce backend. You need to create a Product class that can be compared and sorted. The business logic for comparing products is as follows:

  • Products are primarily compared by their price (lower price is "less than" higher price).
  • If two products have the exact same price, they should be compared alphabetically by their name.

Requirements:

  • Create a Product class with name and price attributes.
  • Use functools.total_ordering to minimize the number of methods you write.
  • Implement __eq__ and __lt__ to handle the price-then-name logic.
  • Verify your implementation by creating a list of Product objects (including some with identical prices) and using the sorted() function to sort them.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.