Migrate to RunxBuild and earn up to $50 in hosting credit on your first deposit.

Calculate your savings
unxBuild
Back to Blog Explainer

Python __str__ vs __repr__: Get This Wrong and Your Logs Are Useless

Sean

Platform Writer

Jul 16, 2026
7 min read

__str__ defines what str(obj) and print(obj) produce - the readable, human-facing description of your object. __repr__ defines what the REPL, the debugger, and the inside of every container show you, and it is meant to be unambiguous rather than pretty. If you only ever define one, define __repr__: Python falls back to __repr__ when __str__ is missing, but never the reverse. Get this backwards and everything looks fine until the day you print a list of your objects into a log during an incident and get a wall of <myapp.models.User object at 0x7f8b1c2d3e40>.

Python __str__ vs __repr__: Get This Wrong and Your Logs Are Useless

This is filed under Python trivia and it should be filed under observability. The methods you define here decide whether your logs are readable at the exact moment you need them to be.

Table of contents

The default is useless, and that is the problem

class User:
    def __init__(self, id, email):
        self.id = id
        self.email = email

u = User(1, "[email protected]")
print(u)
# <__main__.User object at 0x7f8b1c2d3e40>

The memory address tells you nothing. It is not stable across runs, it is not searchable, and if you log a list of a hundred of these you have a hundred lines of noise. The class name is the only useful token in there.

Two methods fix it:

class User:
    def __init__(self, id, email):
        self.id = id
        self.email = email

    def __repr__(self):
        return f"User(id={self.id!r}, email={self.email!r})"

    def __str__(self):
        return self.email

u = User(1, "[email protected]")
print(u)        # [email protected]          <- __str__
u               # User(id=1, email='[email protected]')   <- __repr__
print([u])      # [User(id=1, email='[email protected]')] <- __repr__ again

Which one runs when

The rules, which are worth memorising because they are not intuitive:

  • __str__ - print(obj), str(obj), f-strings by default, format().
  • __repr__ - the REPL, repr(obj), the debugger, and anything inside a container.
  • Fallback - if __str__ is undefined, str() uses __repr__. If __repr__ is undefined, there is no fallback; you get the memory address.

That container rule is the one that bites. Printing a list does not call __str__ on the elements - it calls __repr__:

print(u)     # [email protected]
print([u])   # [User(id=1, email='[email protected]')]

Which is correct, when you think about it. A container’s job is to show you its structure unambiguously, not to render prose. But it means an object with a lovely __str__ and no __repr__ still logs as a memory address the moment it is inside a list, a dict, or a tuple - which in real code is most of the time.

Hence the rule: define __repr__ first. It covers the fallback, it covers containers, and it covers the debugger. __str__ is the optional nicety.

Why this is a logging problem

Here is the scenario that makes this worth an article rather than a footnote.

logger.error("Failed to process orders: %s", failed_orders)
# Failed to process orders: [<app.models.Order object at 0x7f8b1c2d3e40>,
#                           <app.models.Order object at 0x7f8b1c2d3f10>, ...]

It is 2am, orders are failing, and your log tells you that some objects existed at some addresses. The information you needed - which orders - was available and was thrown away, because nobody defined __repr__ on a model class two years ago.

With a __repr__, the same line reads:

Failed to process orders: [Order(id=1041, status='pending'), Order(id=1042, status='pending')]

Now you have IDs to query and a pattern to notice. Same log statement, same code path - the difference is one method defined at leisure months earlier. This is the cheapest observability work available in Python, and it is usually skipped because it looks cosmetic.

What a good repr looks like

The convention from the standard library: __repr__ should look like the code that would recreate the object.

def __repr__(self):
    return f"User(id={self.id!r}, email={self.email!r})"

The !r conversion is the detail people miss - it calls repr() on each field, so strings keep their quotes and you can see the difference between 1 and '1'. Without it, a User(id=1, [email protected]) hides whether id is an int or a string, which is precisely the ambiguity __repr__ exists to remove.

Guidelines that hold up:

  • Include the class name. You will read this next to other objects.
  • Include the identifying fields, not every field. An id and one or two discriminators.
  • Use !r on the values. Ambiguity between types is the thing you are eliminating.
  • Never include secrets. A __repr__ with a password or token in it will end up in a log, an exception, and an error tracker.
  • Never let it raise. A __repr__ that throws turns a small error into an unreadable one, because the exception handler tries to repr the object.

That last point deserves care. If __repr__ accesses a lazily-loaded attribute that hits the database, printing an object in a debugger triggers a query - and in a broken state, that query may fail. Keep __repr__ reading only cheap local attributes.

Dataclasses give you this for free

If your class is mostly data, you should not be writing this by hand:

from dataclasses import dataclass

@dataclass
class User:
    id: int
    email: str

print(repr(User(1, "[email protected]")))
# User(id=1, email='[email protected]')

The generated __repr__ follows exactly the convention above, including !r on the fields. It also gives you __eq__, which you probably wanted too.

One caution: the generated repr includes every field, so a dataclass with a password or token field will print it. Mark those fields field(repr=False):

from dataclasses import dataclass, field

@dataclass
class Credentials:
    username: str
    token: str = field(repr=False)   # kept out of the repr

That one line is the difference between a token in your error tracker and a token not in your error tracker.

How this fits the rest of the stack

Readable logs are the cheapest debugging tool you own, and they are decided long before the incident - by whether someone defined a repr on a model class on a quiet Tuesday. The other half of that equation is whether the logs are somewhere you can actually read them under pressure. When you are weighing what the surrounding platform costs, the RunxBuild hosting calculator shows the service, database, storage, and bandwidth as separate line items, and the RunxBuild dashboard is where the team sees the deploy and the runtime output in one place.

Useful related references:

FAQ

What is the difference between str and repr in Python?

__str__ is the readable, human-facing description used by print() and str(). __repr__ is the unambiguous developer-facing one used by the REPL, the debugger, and anything inside a container. str() falls back to __repr__ when __str__ is undefined, but there is no fallback the other way - so if you define only one, define __repr__.

Which should I define first, str or repr?

__repr__. It covers more ground: Python falls back to it when __str__ is missing, it is what shows up inside lists and dicts, and it is what the debugger displays. Add __str__ only when you have a genuinely different human-readable form worth showing users. For most internal classes, a good __repr__ alone is sufficient.

Why does printing a list not use my str method?

Because containers call __repr__ on their elements, not __str__. A list’s job is to show its structure unambiguously rather than render prose. This is why an object with a nice __str__ and no __repr__ still prints as a memory address inside a list - and why logging a collection of such objects produces useless output.

What should repr return in Python?

By convention, a string resembling the code that would recreate the object: User(id=1, email='[email protected]'). Include the class name and the identifying fields, and use the !r conversion in the f-string so values are repr’d and types stay unambiguous. Never include secrets, and never let it raise - a failing __repr__ makes error handling itself fail.

Do dataclasses generate repr automatically?

Yes. @dataclass generates a __repr__ following the standard convention, including !r on the field values, plus __eq__. Be aware it includes every field by default, so any password or token field will be printed wherever the object is logged. Mark those with field(repr=False) to keep them out.

#__str__ python#python#dunder methods#debugging#dev-infra