PYTHON / OBJECT-ORIENTED PYTHON
Dunder methods: __str__, __repr__, and __eq__
Write __str__, __repr__, and __eq__ so your objects print readably and compare by value, and know which one Python calls where.
What you will learn
- Return a user-facing string from __str__ and a developer-facing one from __repr__
- Rely on __repr__ as the fallback for str(), knowing the reverse never happens
- Compare by value with __eq__ and return NotImplemented for foreign types
- Add __hash__ whenever you define __eq__ on a class you want in sets or dict keys
Understanding Dunder methods: __str__, __repr__, and __eq__
Python never guesses how your object should look as text. When you call print(obj) or str(obj), the interpreter looks up __str__ on the type; when you call repr(obj), or when a container such as a list or dict formats its elements, it looks up __repr__ instead. object.__str__ is implemented by delegating to __repr__, which is why defining only __repr__ gives you sensible output everywhere, while defining only __str__ leaves you with the default <__main__.Money object at 0x7f...> inside lists.
The two methods have different audiences, and that difference should drive what you return. __str__ is for whoever reads the program's output, so it drops implementation detail: "4.50 EUR". __repr__ is for whoever is debugging, so the convention is to return something that looks like the call that would rebuild the object, such as Money(4.5, 'EUR'); using !r on each attribute inside the f-string is what gets the quotes around strings right for free.
Equality works the same way through __eq__, but with one extra rule. Without it, a == b is identity comparison, so two objects holding identical data are unequal. When you define __eq__, return NotImplemented (not False) for types you do not recognise; that is a signal, not an error, and it tells Python to try the other operand's __eq__ before falling back to identity. Python 3 also derives != from your __eq__, so you never write __ne__, but defining __eq__ silently sets __hash__ to None because a mutable notion of equality would break hash tables.
class Money:
def __init__(self, amount, currency):
self.amount = amount
self.currency = currency
def __str__(self):
return f"{self.amount:.2f} {self.currency}"
def __repr__(self):
return f"Money({self.amount!r}, {self.currency!r})"
def __eq__(self, other):
if not isinstance(other, Money):
return NotImplemented
return (self.amount, self.currency) == (other.amount, other.currency)
a = Money(4.5, "EUR")
b = Money(4.5, "EUR")
print(a)
print(repr(a))
print([a, b])
print(a == b, a is b)
print(a == "4.50 EUR")
__str__, __repr__, and __eq__ are hooks the interpreter calls on your behalf in specific places, so what you return decides how your object prints and compares.
Worked examples
The fallback only runs one way
Shows that str() falls back to __repr__, but repr() never falls back to __str__.
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return f"Point(x={self.x}, y={self.y})"
class Tag:
def __init__(self, name):
self.name = name
def __str__(self):
return f"#{self.name}"
p = Point(1, 2)
print(str(p))
print(f"{p}")
t = Tag("python")
print(str(t))
print(repr(t).startswith("<__main__.Tag object at"))
Example explained
Line 1Point defines no __str__, so str(p) reaches object.__str__, which calls __repr__.
Line 2f"{p}" uses format(), whose default for object also routes to __str__, hence the same text.
Line 3Tag defines __str__ only, so repr(t) is still the inherited default with the memory address.
Line 4That asymmetry is why __repr__ is the one to write first if you only write one.
__eq__ removes hashability
Demonstrates that defining __eq__ sets __hash__ to None, and how restoring it makes set deduplication work.
class Card:
def __init__(self, rank, suit):
self.rank = rank
self.suit = suit
def __eq__(self, other):
if not isinstance(other, Card):
return NotImplemented
return self.rank == other.rank and self.suit == other.suit
print(Card.__hash__)
try:
{Card("A", "spades")}
except TypeError as exc:
print("TypeError:", exc)
class HashableCard(Card):
def __hash__(self):
return hash((self.rank, self.suit))
deck = {HashableCard("A", "spades"), HashableCard("A", "spades")}
print(len(deck))
print(HashableCard("A", "spades") in deck)
Example explained
Line 1Card.__hash__ prints None: Python replaced the inherited hash the moment __eq__ appeared.
Line 2Putting a Card in a set therefore raises TypeError rather than using identity hashing.
Line 3HashableCard hashes the same fields __eq__ compares, so equal cards land in the same bucket.
Line 4The set collapses two equal cards to one entry, and `in` finds a third object built from the same data.
Where __eq__ is used implicitly
Shows that list membership, count, index, and remove all call __eq__ rather than comparing identity.
class Version:
def __init__(self, major, minor):
self.major = major
self.minor = minor
def __repr__(self):
return f"Version({self.major}, {self.minor})"
def __eq__(self, other):
if not isinstance(other, Version):
return NotImplemented
return (self.major, self.minor) == (other.major, other.minor)
releases = [Version(1, 0), Version(1, 2), Version(1, 0)]
print(Version(1, 2) in releases)
print(releases.count(Version(1, 0)))
print(releases.index(Version(1, 0)))
releases.remove(Version(1, 0))
print(releases)
print(Version(1, 0) != Version(1, 2))
Example explained
Line 1`in` scans the list calling __eq__ on each element, so a freshly built Version matches.
Line 2count returns 2 because equality, not identity, decides what counts as the same value.
Line 3remove deletes only the first match, leaving the later equal element in place.
Line 4!= is True without any __ne__ method: Python inverts the result of __eq__.
Important notes
__str__ and __repr__ must return str; returning an int or None raises TypeError at the call site, not at class definition time.
__eq__ gives you == and != only. Sorting or <, >, <= need __lt__ and friends, or the functools.total_ordering decorator on top of __eq__ plus one ordering method.
Common mistakes
Defining only __str__ and expecting lists to print nicely; list formatting uses repr, so you get <__main__.Card object at 0x...> for every element.
Returning False instead of NotImplemented for unrelated types in __eq__, which blocks the other object's __eq__ from running and breaks comparisons with types designed to interoperate with yours.
Adding __eq__ to a class already used as a dict key or set member, then getting TypeError: unhashable type because __hash__ was set to None.
Try it yourself
Change, predict, then run
Write a Duration class holding total seconds, with __str__ returning "1h 05m", __repr__ returning Duration(3900), and __eq__ comparing the seconds. Print one instance, print a list of two equal instances, and confirm they compare equal while `is` is False.
Open the Python workspaceCheck your understanding
A class defines __str__ but not __repr__. What does print([obj]) show for one instance?
- The text returned by __str__, since print always uses __str__
- The default <__main__.X object at 0x...> form
- A TypeError, because __repr__ is missing
- The class name X with no attribute values
Show answer
A list builds its own display by calling repr() on each element, and repr() has no fallback to __str__, so the inherited object.__repr__ runs and prints the address form. The first option is tempting because print(obj) really does use __str__, but here print receives a list; __str__ of the list is what formats the elements, and it uses repr for them.