Home Tutorials OOP

OOP

Python's __repr__ and __str__: Controlling How Objects Print

Pyford Notes July 6, 2026 6 min read
Key points
  • The default repr of a custom object shows only its class name and memory address, which tells you almost nothing while debugging.
  • __repr__ is aimed at developers; ideally it looks like valid Python that could recreate the object.
  • __str__ is aimed at end users and is what print() and str() use, falling back to __repr__ if it's not defined.
  • Containers like lists and dicts always call repr() on their elements, never str(), even inside print().

Default repr and why it's not useful

Print an instance of a plain class and Python shows something like this:

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

p = Point(3, 4)
print(p)   # <__main__.Point object at 0x7f2a3c0b5d90>

That's the inherited default from object.__repr__: the class name and a memory address. It confirms the object exists and its type, and that's about it — two different Point instances with identical coordinates print as two different, equally uninformative strings. In a debugging session with a dozen objects logged, this default is close to useless.

Defining __repr__ for debugging

Overriding __repr__ fixes this directly. The convention, from the official documentation, is that repr() should ideally return a string that, if fed back into eval(), would recreate an equal object:

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __repr__(self):
        return f"Point(x={self.x!r}, y={self.y!r})"

p = Point(3, 4)
print(p)          # Point(x=3, y=4)
print([p, p])     # [Point(x=3, y=4), Point(x=3, y=4)]

That convention isn't a strict requirement — plenty of reasonable __repr__ implementations aren't literally eval-able, especially for objects wrapping file handles or network connections — but it's a good default to aim for because it forces you to include the fields that actually distinguish one instance from another. The !r conversion in the f-string matters: it calls repr() on self.x rather than str(), so a string field shows its quotes, which stops Point(x=3, y=4) from being ambiguous with Point(x='3', y='4').

Defining __str__ for end users

__str__ is what gets called by print(), str(), and f-string interpolation without !r. It's meant for output a non-developer might actually read:

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __repr__(self):
        return f"Point(x={self.x!r}, y={self.y!r})"

    def __str__(self):
        return f"({self.x}, {self.y})"

p = Point(3, 4)
print(p)         # (3, 4) -- __str__
print(repr(p))   # Point(x=3, y=4) -- __repr__

Not every class needs both. A data-heavy class used mostly in logs and debugging sessions often only needs __repr__. A class that represents something meant for a user-facing message — a currency amount, a formatted date, a validation error — benefits from a distinct __str__ that drops the internal structure and shows just the meaningful value, in the same spirit as the custom exception classes covered in the custom exceptions guide, where a clear message matters as much as the exception type.

Why str() falls back to __repr__

If a class defines __repr__ but not __str__, calling str() or print() on an instance uses __repr__ instead of falling back to the useless default:

class Point:
    def __init__(self, x, y):
        self.x, self.y = x, y

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

print(Point(1, 2))   # Point(1, 2) -- str() fell back to __repr__

This is why defining just __repr__ is a reasonable minimum for most classes: it immediately improves both debugging output and casual print() calls, with no extra code. Defining __str__ only becomes worth the effort when the developer-facing and user-facing representations genuinely need to differ.

repr inside lists, dicts, and f-strings

A detail that surprises people the first time: printing a list of objects never calls __str__ on the elements, only __repr__, even though the outer print() call itself uses str() on the list:

points = [Point(1, 2), Point(3, 4)]
print(points)   # [Point(1, 2), Point(3, 4)] -- reprs, not strs

d = {"origin": Point(0, 0)}
print(d)        # {'origin': Point(0, 0)} -- repr again, note the quoted key too

Containers use repr() for their elements because a container's own str() is really its repr() — lists and dicts don't define a separate __str__ at all. This is also why string keys in a printed dict show their quotes, and why it's worth writing a genuinely useful __repr__ even for classes where you assumed only __str__ would ever be seen in practice; nearly every debugging session ends up printing a container full of your objects sooner or later. The Python data model documentation covers the exact contract for both methods in more depth.