Python’s __str__ and __repr__ methods are often confused, but they serve distinct purposes: __str__ is for human-readable output, while __repr__ is for unambiguous developer representation.
Let’s see them in action. Imagine we have a Point class representing a 2D coordinate:
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __str__(self):
return f"({self.x}, {self.y})"
def __repr__(self):
return f"Point(x={self.x}, y={self.y})"
p = Point(10, 20)
When you print p or use str(p), Python calls __str__:
>>> print(p)
(10, 20)
This is what a user might see. Now, when you inspect p in an interactive session or use repr(p), Python calls __repr__:
>>> p
Point(x=10, y=20)
The goal of __repr__ is to be unambiguous, ideally producing output that, if fed back into Python, would recreate the object. This is why Point(x=10, y=20) is more useful than just (10, 20), which could be a tuple.
The __eq__ method is for defining equality between objects. By default, two instances of the same class are considered equal only if they are the exact same object in memory (identity equality). __eq__ allows you to define what it means for two objects to be equivalent based on their attributes.
Let’s add __eq__ to our Point class:
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __str__(self):
return f"({self.x}, {self.y})"
def __repr__(self):
return f"Point(x={self.x}, y={self.y})"
def __eq__(self, other):
if not isinstance(other, Point):
# Don't attempt to compare against unrelated types
return NotImplemented
return self.x == other.x and self.y == other.y
p1 = Point(10, 20)
p2 = Point(10, 20)
p3 = Point(30, 40)
print(p1 == p2)
print(p1 == p3)
Output:
True
False
Here, p1 and p2 are distinct objects in memory, but __eq__ tells Python they are equal because their x and y attributes match. p1 and p3 are not equal because their attributes differ. The isinstance check and returning NotImplemented is crucial for correct behavior when comparing with objects of different types.
The system of dunder (double underscore) methods, also known as "magic methods" or "special methods," allows Python objects to integrate seamlessly with Python’s built-in operations and syntax. They are the mechanism by which Python’s language features, like print(), len(), +, ==, and iteration, are applied to your custom objects. You’re not just defining methods; you’re defining how your objects behave within the Python ecosystem.
When you define __repr__, if you don’t define __str__, Python will fall back to using __repr__ for str() conversions. This is a common pattern: implement __repr__ thoroughly for debugging, and then optionally add __str__ for a more user-friendly representation if needed. If neither is present, you get a default, often unhelpful, representation like <__main__.Point object at 0x10a4f39d0>.
The real power of __eq__ comes into play when you consider it in conjunction with hashing. If you define __eq__ and want your objects to be usable as keys in dictionaries or elements in sets (which require hashable objects), you must also define a __hash__ method. The rule is: if two objects are equal according to __eq__, their __hash__ values must be the same. If you don’t define __hash__ and Python sees you’ve defined __eq__, it will automatically make your objects unhashable by setting __hash__ = None. A common way to implement __hash__ is to hash a tuple of the attributes that are used in __eq__.
Understanding how __str__, __repr__, and __eq__ work together is fundamental to creating well-behaved and intuitive Python objects. They are not just cosmetic; they dictate how your objects interact with the language’s core functionalities, from simple printing to complex data structures.
The next logical step in exploring dunder methods is understanding how they enable custom behavior for arithmetic operations, like addition and subtraction, through methods like __add__ and __sub__.