__repr__ and __eq__

The dunder methods that let an object control how it prints for debugging and how it compares for equality with ==.

                def __repr__(self) -> str: ...
def __eq__(self, other) -> bool: ...

              

Purpose

__repr__ and __eq__ are two of Python’s dunder methods (“double underscore”) — the mechanism by which a custom class participates in built-in behavior. Define __repr__ and print(obj) (and the REPL, and a debugger) shows something useful instead of a bare memory address; define __eq__ and == compares by value instead of by identity.

Example

class Point:
    def __init__(self, x: int, y: int) -> None:
        self.x = x
        self.y = y

    def __repr__(self) -> str:
        return f"Point(x={self.x}, y={self.y})"

    def __eq__(self, other: object) -> bool:
        if not isinstance(other, Point):
            return NotImplemented
        return self.x == other.x and self.y == other.y
p1 = Point(1, 2)
p2 = Point(1, 2)
print(p1)          # Point(x=1, y=2) — thanks to __repr__
print(p1 == p2)     # True — thanks to __eq__, even though p1 is not p2
print(p1 is p2)     # False — still two distinct objects

Behavior notes

  • Without a custom __repr__, a class’s default is something like <__main__.Point object at 0x...> — technically correct, practically useless for debugging.
  • Without a custom __eq__, == falls back to identity comparison — behaving exactly like is — which is almost never what a value-like class (a point, a book, a coordinate) actually wants.
  • Returning NotImplemented (not False) when other isn’t a comparable type lets Python fall back to trying the comparison from the other object’s side, rather than asserting “definitely not equal” prematurely.
  • @dataclass generates both of these automatically from a class’s fields — writing them by hand, as shown above, is what it does for you.

Common mistakes

  • Defining __eq__ without also considering __hash__ — Python sets __hash__ to None automatically on any class that defines __eq__ without also defining __hash__, making instances unhashable (unusable as dict keys or set members) unless __hash__ is restored deliberately. @dataclass handles this trade-off consistently; hand-written classes must decide it themselves.
  • Comparing type(other) == Point instead of isinstance(other, Point) in __eq__ — the latter correctly handles subclasses; the former silently rejects them.

Implementation notes

Dunder-method dispatch (print() calling __repr__, == calling __eq__) is defined by the language’s data model — identical across conforming implementations, not a CPython-specific convention.

Taught in these lessons

Related reference