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:
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.yThe 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:
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.yThe 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:
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:
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
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.
Frequently Asked Questions
What is the difference between is and == in Python?
How do I make my objects work with comparison operators?
What is the relationship between __eq__ and __hash__?
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.
More in this topic
Create Custom Iterators in Python
Learn how to build your own iterators in Python by implementing the __iter__() and __next__() methods, with practical examples and reusable patterns.
Python Iterator Protocol Explained
Learn how Python's iterator protocol works with the __iter__() and __next__() methods, and understand the contract every iterator must follow.
Python Iterables and Iterators Explained
Learn what iterables and iterators are in Python, how they power for loops under the hood, and why understanding them unlocks cleaner data processing patterns.