Wednesday, July 15, 2026

Understanding Python Dunder Methods: __str__ vs __repr__

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 + other triggers __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

ContextMethod 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

  1. Always implement __repr__. It's your safety net for debugging, logging, and the REPL.
  2. Implement __str__ only when a different, friendlier output makes sense for end users.
  3. Make __repr__ unambiguous — ideally valid Python that recreates the object, or at least clearly labeled with class name and field values.
  4. Use !r in f-strings when building __repr__ to correctly quote string fields.
  5. 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