Object Comparison in Python

Learn how Python compares objects using equality, identity, and rich comparison methods, and how to implement custom comparison logic for your classes.

6 min read

Python object comparison is the set of operations that determine whether two objects are equal, whether one is less than another, and whether an object can be used as a dictionary key or stored in a set. These operations rely on magic methods that you can implement on your classes to define what equality, ordering, and hashing mean for your objects. By default, Python compares objects by identity: two objects are equal only if they are the same object in memory. For most user-defined classes, this default is not useful because two distinct objects that represent the same value should compare as equal. A Point at coordinates (3, 7) should equal another Point at (3, 7), even if they are different objects in memory. Implementing the comparison magic methods gives your classes semantics that match their purpose.

The article on operator overloading with Python magic methods introduced the arithmetic and comparison magic methods at a high level. This article goes deeper into object comparison specifically, covering the relationship between identity and equality, the six rich comparison methods, and the critical connection between equality and hashing that determines whether your objects can be stored in sets and used as dictionary keys. Understanding these concepts is essential for writing classes that work correctly with Python's core data structures.

Comparison in Python involves three distinct concepts that are often confused. Identity, checked with the is operator, asks whether two names refer to the exact same object. Equality, checked with the double equals operator, asks whether two objects represent the same value according to the class's definition. Hashing, used by dictionaries and sets, requires that equal objects produce the same hash value. Each concept builds on the previous one, and implementing them correctly for your classes is a matter of following well-established patterns.

Equality with eq

The equality method defines what it means for two objects of your class to be considered equal. Python calls this method when the double equals operator is used between two objects, and the default implementation inherited from object compares by identity. Overriding this method lets you define equality based on the values of the object's attributes rather than its memory address.

Here is a class that represents a point in two-dimensional space and defines equality based on the coordinates:

pythonpython
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

The method first checks whether the other object is a Point instance. If it is not, returning NotImplemented tells Python to try the reverse comparison or fall back to False. This type check is important for interoperability: without it, comparing a Point to a tuple would raise an AttributeError when the method tries to access other.x on a tuple object. With the type check, the comparison gracefully returns False or allows Python to try the tuple's equality method.

After implementing equality, two Point objects with the same coordinates compare as equal, which is the intuitive behavior. However, these Point objects are no longer hashable because Python sets the hash to None when equality is overridden without a corresponding hash method. The next section covers how to restore hashability.

Ordering with rich comparison methods

Python provides six rich comparison methods for ordering: less than, less than or equal, greater than, greater than or equal, equality, and inequality. You do not need to implement all six. Implementing equality and one ordering method, typically less than, is sufficient for most use cases, and Python can infer the inverse of inequality from equality. For more complete ordering support, the functools.total_ordering decorator can generate the remaining methods automatically.

Here is a version of the Point class that adds ordering based on distance from the origin, using total_ordering to minimize boilerplate:

pythonpython
from functools import total_ordering
 
@total_ordering
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

The total_ordering decorator will generate the missing comparison methods from equality and one ordering method. Here is the less-than method that compares points by their distance from the origin:

pythonpython
    def __lt__(self, other):
        if not isinstance(other, Point):
            return NotImplemented
        return (self.x ** 2 + self.y ** 2) < (other.x ** 2 + other.y ** 2)

The total_ordering decorator generates the missing comparison methods from equality and less than. With only two methods written, the class supports all six comparison operators, which means Point objects can be sorted, used with min and max, and compared with greater than, less than or equal, and other operators. The decorator saves boilerplate at the cost of a small runtime overhead for the generated methods, which call the two base methods internally.

Hashing and dictionary key compatibility

If you override equality, you must also consider hashing. Python requires that equal objects produce equal hash values, and it enforces this by automatically setting the hash method to None when equality is overridden. Objects with a None hash cannot be stored in sets or used as dictionary keys, which is Python's way of preventing the subtle bugs that occur when equal objects produce different hashes.

To restore hashability, implement a hash method that computes a hash from the same attributes used in the equality comparison. The simplest approach is to hash a tuple of those attributes:

pythonpython
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
 
    def __hash__(self):
        return hash((self.x, self.y))

The hash method returns the hash of a tuple containing the x and y coordinates. Because tuples are hashable and their hash is computed from their contents, two Points with the same coordinates will produce the same hash, satisfying Python's requirement. The objects can now be stored in sets and used as dictionary keys, and the dictionary will correctly treat two distinct Point objects with the same coordinates as the same key.

The relationship between equality and hashing only matters if your objects need to be used in sets or as dictionary keys. If your objects are only compared for equality and never stored in hash-based collections, overriding equality without hash is acceptable, and the error message when someone tries to use the object as a key will be clear. The article on class variables and instance variables covers how attributes at different levels of a class interact, and the equality and hash methods typically operate on instance attributes.

Rune AI

Rune AI

Key Insights

  • The is operator checks object identity; the == operator checks object equality through the eq method.
  • Implement eq to define what it means for two objects of your class to be considered equal.
  • Implement lt and related methods to enable sorting and ordering comparisons.
  • If you override eq, you must also implement hash if you want your objects to be usable as dictionary keys or set members.
  • The functools.total_ordering decorator can generate missing comparison methods from eq and one ordering method.
RunePowered by Rune AI

Frequently Asked Questions

What is the difference between is and == in Python?

The is operator checks identity: it returns True only if two names refer to the exact same object in memory. The == operator checks equality: it calls the __eq__ method and returns True if the objects are considered equal according to the class's definition of equality. Two distinct objects with the same data will compare equal with == but not with is. For immutable value types like integers and strings, Python may reuse objects for efficiency, making is appear to work for equality checks, but relying on this behavior is a common source of subtle bugs.

How do I make my objects work with comparison operators?

Implement the rich comparison magic methods: __eq__ for equality, __ne__ for inequality, __lt__ for less than, __le__ for less than or equal, __gt__ for greater than, and __ge__ for greater than or equal. You do not need to implement all six. Python can infer __ne__ from __eq__, and the functools.total_ordering decorator can fill in the remaining ordering methods if you implement __eq__ and one of __lt__, __le__, __gt__, or __ge__.

What is the relationship between __eq__ and __hash__?

If you override __eq__ without overriding __hash__, Python sets __hash__ to None, making your objects unhashable. This means they cannot be used as dictionary keys or stored in sets. If two objects compare equal, they must have the same hash value. If you want your objects to be hashable after implementing custom equality, you must also implement __hash__ in a way that is consistent with your equality logic. Typically, you compute the hash from the same attributes used in the equality comparison.

Conclusion

Object comparison is one of the most fundamental operations in Python, and implementing it correctly on your custom classes makes them work naturally with dictionaries, sets, sorting functions, and conditional logic. The key distinctions are identity versus equality, the six rich comparison methods, and the relationship between equality and hashing that determines whether your objects can be used as dictionary keys.