Python
Object-Oriented Programming
Equality Comparison
Attribute Comparison
Software Development

Compare object instances for equality by their attributes

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

When you compare two objects in Python using ==, the default behavior checks whether they are the exact same object in memory (reference equality). In most real-world applications, you want to know whether two objects have the same attribute values instead (value equality). This article explains how to implement attribute-based equality comparison in Python, covers the relationship between __eq__ and __hash__, and walks through common mistakes.

Reference Equality vs. Value Equality

By default, Python's == operator for custom classes checks identity, which is the same as using is:

python
1class Point:
2    def __init__(self, x, y):
3        self.x = x
4        self.y = y
5
6p1 = Point(1, 2)
7p2 = Point(1, 2)
8
9print(p1 == p2)  # False -- different objects in memory
10print(p1 is p2)  # False -- same result

Even though p1 and p2 have identical attribute values, they are two separate objects and the comparison returns False. To fix this, you need to define the __eq__ method.

Implementing __eq__ for Value Equality

The __eq__ method lets you define what "equal" means for instances of your class:

python
1class Point:
2    def __init__(self, x, y):
3        self.x = x
4        self.y = y
5
6    def __eq__(self, other):
7        if not isinstance(other, Point):
8            return NotImplemented
9        return self.x == other.x and self.y == other.y
10
11p1 = Point(1, 2)
12p2 = Point(1, 2)
13p3 = Point(3, 4)
14
15print(p1 == p2)  # True
16print(p1 == p3)  # False

Returning NotImplemented instead of False when other is a different type allows Python to try the comparison from the other side. This is important for interoperability with subclasses and other types.

The __eq__ and __hash__ Relationship

In Python, when you define __eq__, the default __hash__ method is automatically set to None. This means objects of your class become unhashable and cannot be used as dictionary keys or set members:

python
1class Point:
2    def __init__(self, x, y):
3        self.x = x
4        self.y = y
5
6    def __eq__(self, other):
7        if not isinstance(other, Point):
8            return NotImplemented
9        return self.x == other.x and self.y == other.y
10
11p = Point(1, 2)
12my_set = {p}  # TypeError: unhashable type: 'Point'

To restore hashability, implement __hash__ using the same attributes that __eq__ compares:

python
1class Point:
2    def __init__(self, x, y):
3        self.x = x
4        self.y = y
5
6    def __eq__(self, other):
7        if not isinstance(other, Point):
8            return NotImplemented
9        return self.x == other.x and self.y == other.y
10
11    def __hash__(self):
12        return hash((self.x, self.y))
13
14p1 = Point(1, 2)
15p2 = Point(1, 2)
16
17print({p1, p2})  # {Point(1, 2)} -- one element, since they are equal

The rule is: objects that compare equal must have the same hash value. If __eq__ uses x and y, then __hash__ must also use x and y.

Using dataclasses for Automatic Equality

Python 3.7+ provides dataclasses that generate __eq__ (and optionally __hash__) automatically:

python
1from dataclasses import dataclass
2
3@dataclass
4class Point:
5    x: float
6    y: float
7
8p1 = Point(1, 2)
9p2 = Point(1, 2)
10
11print(p1 == p2)  # True -- __eq__ is auto-generated

By default, @dataclass generates __eq__ but not __hash__ (because instances are mutable). To make instances hashable, use frozen=True:

python
1@dataclass(frozen=True)
2class Point:
3    x: float
4    y: float
5
6points = {Point(1, 2), Point(1, 2), Point(3, 4)}
7print(len(points))  # 2

Frozen dataclasses are immutable and hashable, making them ideal for use as dictionary keys or set members.

Comparing All Attributes Generically

If you want to compare all attributes without listing each one, you can use __dict__:

python
1class Config:
2    def __init__(self, host, port, debug):
3        self.host = host
4        self.port = port
5        self.debug = debug
6
7    def __eq__(self, other):
8        if not isinstance(other, Config):
9            return NotImplemented
10        return self.__dict__ == other.__dict__
11
12c1 = Config("localhost", 5432, True)
13c2 = Config("localhost", 5432, True)
14print(c1 == c2)  # True

This is convenient but has caveats. It compares every attribute, including ones you may want to exclude (like internal caches or timestamps). It also does not work with __slots__ classes.

Equality with Inheritance

When your classes form an inheritance hierarchy, equality checks need extra care:

python
1class Shape:
2    def __init__(self, color):
3        self.color = color
4
5    def __eq__(self, other):
6        if type(other) is not type(self):
7            return NotImplemented
8        return self.color == other.color
9
10class Circle(Shape):
11    def __init__(self, color, radius):
12        super().__init__(color)
13        self.radius = radius
14
15    def __eq__(self, other):
16        if type(other) is not type(self):
17            return NotImplemented
18        return self.color == other.color and self.radius == other.radius

Using type(other) is not type(self) instead of isinstance prevents a Shape from being considered equal to a Circle that happens to have the same color. This preserves symmetry: if a == b, then b == a.

Common Pitfalls

  • Forgetting __hash__: Defining __eq__ without __hash__ makes your objects unusable in sets and as dictionary keys. Python will raise a TypeError.
  • Mutable attributes in __hash__: If you include mutable attributes in __hash__ and then change them, the object becomes "lost" in any set or dict it was added to.
  • Breaking symmetry with isinstance: Using isinstance in __eq__ can create asymmetric comparisons between parent and child classes.
  • Comparing floats directly: Floating-point attributes should be compared with a tolerance (using math.isclose()) rather than strict equality.

Summary

To compare objects by their attributes in Python, implement __eq__ to define what equality means for your class. Always implement __hash__ alongside __eq__ if you need your objects in sets or dictionaries. For simple data-holding classes, use @dataclass to get __eq__ for free. Be mindful of symmetry, transitivity, and the implications of inheritance when designing equality logic.


Course illustration
Course illustration

All Rights Reserved.