Python gives every object two dunder methods that decide how it behaves in two places you touch constantly: __repr__ controls what you see when you print or inspect the object, and __eq__ controls what the == operator means. Overriding them turns an opaque object into one that reads well and compares correctly.
Without them, a custom class prints as something like <__main__.Point object at 0x7f9c1a2b3c40> and compares equal only to itself. That default is not a bug — it is the honest behaviour of a class that has not said anything about identity or value. Once you define __eq__, you have made a promise about what your objects mean.
What does Python print for a plain object?
Without a __repr__, Python falls back to object.__repr__, which prints the class name and the memory address of the instance. That address is unique while the object lives, so it is fine for confirming "these are two different objects" and useless for anything else. It also changes between runs, which makes it useless in a snapshot test.
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
first = Point(1, 2)
second = Point(1, 2)
print(first)
print(second)
print("the addresses are different:", first is not second)
The address differs on every run, so if you are writing tests that compare printed output, the default representation guarantees they will fail.
What makes a good __repr__?
A useful __repr__ shows the object's state in a form a reader can picture, and ideally in a form they could paste back into the constructor. repr() is what print() uses for objects that have no __str__, and it is what the interactive prompt and repr() itself call.
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return f"Point({self.x}, {self.y})"
p = Point(3, 4)
print(p)
print(repr(p))
rebuilt = eval(repr(p))
print("pasteable back into the constructor:", rebuilt.x, rebuilt.y)
There is a related method, __str__. It exists for humans rather than for developers: print() prefers it when present, while the interactive prompt and containers fall back to __repr__. The usual convention is that __repr__ is precise and unambiguous, and __str__ is a friendlier summary. If you define only __repr__, that is perfectly acceptable — __str__ will simply not exist and printing will use your __repr__.
| Situation | Which method runs |
|---|---|
print(obj) | __str__ if defined, otherwise __repr__ |
| the interactive prompt, or a list of your objects | __repr__, always |
repr(obj) | __repr__, always |
str(obj) | __str__ if defined, otherwise __repr__ |
How does equality work before you define __eq__?
By default, == on two objects asks whether they are the same object. This is identity comparison, the same question is asks. It is the correct behaviour for mutable objects, where two separate but equal-looking items are genuinely different things.
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
a = Point(1, 2)
b = Point(1, 2)
c = Point(9, 9)
print("a == b:", a == b)
print("a is b:", a is b)
print("a == c:", a == c)
print("lists compare by value:", [1, 2] == [1, 2])
That is why [1, 2] == [1, 2] is True while two separately built Point objects are not equal. Lists implement __eq__ to compare element by element; your class does not, unless you say so.
Identity comparison is the right default for mutable objects, and it is the wrong default for value objects. If two Point objects describe the same place, you almost always want them to compare equal even though they live at different memory addresses. That is exactly the gap __eq__ fills.
How do you implement __eq__ correctly?
Compare the attributes that define the object's value, and return NotImplemented when the comparison does not apply rather than raising. NotImplemented tells Python "I cannot answer this one", and Python then tries the reflected operation on the other operand. That is what lets your class compare cleanly against other types.
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, self.y) == (other.x, other.y)
print(Point(1, 2) == Point(1, 2))
print(Point(1, 2) == Point(1, 3))
print("comparing with something unrelated:", Point(1, 2) == (1, 2))
print("membership now works:", Point(1, 2) in [Point(1, 2), Point(5, 5)])
There is a consequence of defining __eq__ that catches everybody once: Python sets __hash__ to None automatically, and the object becomes unhashable. That is deliberate. If two objects can be equal, they must have the same hash, and Python cannot know whether your mutable attributes would change. If your objects are immutable, define __hash__ yourself so they can still go into sets and be used as dictionary keys.
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, self.y) == (other.x, other.y)
try:
{Point(1, 2)}
except TypeError as exc:
print("unhashable:", exc)
class ImmutablePoint:
def __init__(self, x, y):
self.x = x
self.y = y
def __eq__(self, other):
if not isinstance(other, ImmutablePoint):
return NotImplemented
return (self.x, self.y) == (other.x, other.y)
def __hash__(self):
return hash((self.x, self.y))
bucket = {ImmutablePoint(1, 2), ImmutablePoint(1, 2), ImmutablePoint(3, 4)}
print("set size:", len(bucket))
What about ordering and testing membership?
Defining __eq__ does not make < work. For that you define __lt__ (and the other comparison operators as needed). If you want all of them from a single definition, the functools.total_ordering decorator fills in the rest based on __eq__ and one ordering method.
The decorator has one requirement: the class must define __eq__ and exactly one ordering method. It then generates the remaining ones, so <=, > and >= all follow consistently from your __lt__ and your __eq__. Writing all four by hand is also fine and sometimes clearer — the decorator just saves you the repetition.
Membership testing with in uses __eq__ on the elements, so once __eq__ works, x in my_list works too. That one change quietly fixes a lot of code that used any(item == target for item in items).
Cheat sheet
- The default
reprshows a memory address that changes every run. Override it. __repr__is for developers and__str__for humans; defining only__repr__is fine.- Default
==is identity. Define__eq__to compare by value. - Return
NotImplementedfor comparisons that do not apply instead of raising. - Defining
__eq__makes instances unhashable unless you also define__hash__.
Try it yourself: a value object that behaves properly
Build a Money class holding an amount (a Decimal or an integer number of minor units) and a three-letter currency. Give it a __repr__ like Money(1250, 'GBP'), a __str__ like "GBP 12.50", and an __eq__ that compares both attributes.
Check that two separately built Money objects with the same values are equal, and that adding a third to a set raises a clear error because __hash__ is missing. Then define __hash__ based on the tuple of both attributes and confirm the set works. Finally, add __lt__ and check that a list of Money objects sorts correctly.