Introduction
If you've spent any time writing Python classes, you've probably noticed something odd: print an object and you get a memory address like <__main__.Point object at 0x7f8b1c0a5d90>. It's not exactly useful. This is where two of Python's most important "dunder" (double underscore) methods come in — __str__ and __repr__.
They look similar, get confused constantly, and yet they serve genuinely different purposes. Let's clear that up.
What Are Dunder Methods, Anyway?
"Dunder" is short for "double underscore." Methods like __init__, __len__, __add__, __str__, and __repr__ are special methods Python calls automatically in response to built-in operations. You rarely call them directly — instead, Python's syntax and built-in functions trigger them behind the scenes.
obj + othertriggers__add__len(obj)triggers__len__print(obj)triggers__str__repr(obj)(and the interactive shell) triggers__repr__
This system is often called "dunder methods" or the "data model," and it's what lets custom objects behave like built-in types.
The Default Behavior (Why You Need These)
Without any customization, printing an object gives you almost nothing useful:
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
p = Point(3, 4)
print(p)
# <__main__.Point object at 0x7f8b1c0a5d90>
That's technically "correct," but useless for debugging or logging. Defining __str__ and __repr__ fixes this.
__repr__: The Developer's View
__repr__ should return a string that is unambiguous and, ideally, could be used to recreate the object. Think of it as the representation you'd want to see in a debugger, a log file, or the Python REPL.
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})"
p = Point(3, 4)
p
# Point(x=3, y=4)
repr(p)
# 'Point(x=3, y=4)'
The guiding principle (straight from Python's own documentation philosophy) is:
eval(repr(obj)) == obj should ideally hold true.
In our example, Point(x=3, y=4) is literally valid Python code that recreates the object — that's a well-behaved __repr__.
__str__: The User's View
__str__ is meant to return a readable, human-friendly string — something you'd show to an end user, not a developer debugging the internals.
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})"
def __str__(self):
return f"({self.x}, {self.y})"
p = Point(3, 4)
print(p) # calls __str__
# (3, 4)
print(repr(p)) # calls __repr__
# Point(x=3, y=4)
Notice the difference in intent: __str__ gives a clean coordinate pair for a user-facing message, while __repr__ gives full detail for debugging.
What Happens If You Only Define One?
This is the part that trips people up. Python has a fallback rule:
If __str__ is not defined, Python falls back to __repr__.
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})"
p = Point(3, 4)
print(p)
# Point(x=3, y=4) <- uses __repr__ since __str__ isn't defined
The reverse is not true — if you only define __str__, repr(obj) still falls back to the default <Point object at 0x...> unless you explicitly define __repr__.
This is why the common advice is: always define __repr__. Define __str__ only if you need a different, more user-friendly output.
Where Each One Actually Gets Used
| Context | Method called |
|---|---|
print(obj) | __str__ (falls back to __repr__) |
str(obj) | __str__ (falls back to __repr__) |
repr(obj) | __repr__ |
Interactive REPL (typing obj and hitting Enter) | __repr__ |
Inside a list/dict: print([obj1, obj2]) | __repr__ (containers always use repr on their elements) |
Debuggers, logging, f"{obj!r}" | __repr__ |
f"{obj}" or f"{obj!s}" | __str__ |
That container detail is worth calling out explicitly — it surprises a lot of people:
points = [Point(1, 2), Point(3, 4)]
print(points)
# [Point(x=1, y=2), Point(x=3, y=4)]
Even though Point has a __str__, printing a list of points uses __repr__ for each element, because containers always show the repr of their contents.
A Practical, Real-World Example
Here's a slightly more realistic class — a simple User model — showing both methods pulling their proper weight:
class User:
def __init__(self, username, email, is_active=True):
self.username = username
self.email = email
self.is_active = is_active
def __repr__(self):
return (f"User(username={self.username!r}, "
f"email={self.email!r}, is_active={self.is_active})")
def __str__(self):
status = "active" if self.is_active else "inactive"
return f"{self.username} ({status})"
user = User("mchen", "mchen@example.com")
print(user)
# mchen (active) <- friendly, for UI/logs shown to humans
print(repr(user))
# User(username='mchen', email='mchen@example.com', is_active=True)
# <- precise, for debugging
users = [user, User("jsmith", "jsmith@example.com", is_active=False)]
print(users)
# [User(username='mchen', ...), User(username='jsmith', ...)]
# <- repr used automatically in the list
Notice the !r inside the f-string in __repr__ — that forces the repr() of self.username and self.email, wrapping strings in quotes. This is a small but important habit: it keeps your repr unambiguous (you can tell a string field apart from a number or None at a glance).
Quick Rules of Thumb
- Always implement
__repr__. It's your safety net for debugging, logging, and the REPL. - Implement
__str__only when a different, friendlier output makes sense for end users. - Make
__repr__unambiguous — ideally valid Python that recreates the object, or at least clearly labeled with class name and field values. - Use
!rin f-strings when building__repr__to correctly quote string fields. - Remember containers use
__repr__on their elements, not__str__.
Wrapping Up
__str__ and __repr__ aren't just cosmetic — they're part of how Python objects communicate with the people who use and debug them. __repr__ is for developers: precise, unambiguous, ideally reconstructable. __str__ is for everyone else: clean and readable. Define both thoughtfully, and your objects will be far more pleasant to work with — whether you're staring at a log file at 2 AM or showing output to an actual user.
Written by Munia Balayil
