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__: How Objects Print, and Why You Probably Want __repr__ Instead

Sean

Platform Writer

Aug 07, 2026
8 min read

str defines what your object looks like to a human — it is what print() and str() call — while repr defines what it looks like to a developer, and Python falls back to repr when str is missing but never the other way around.

Python __str__: How Objects Print, and Why You Probably Want __repr__ Instead

That asymmetry is the whole reason the advice is what it is. If you only implement one, implement __repr__, because it covers both cases. Implement only __str__ and your debugger, your REPL, and every list of your objects still print memory addresses.

Table of contents

The default, and why it is useless

class Order:
    def __init__(self, order_id, total):
        self.order_id = order_id
        self.total = total

print(Order("A-1029", 49.99))
# <__main__.Order object at 0x7f8b1c0d3fd0>

That address tells you the class and nothing else. It is the same output whether the order is correct or catastrophically wrong, which is exactly the moment you are usually looking at it.

It gets worse inside a container, because containers use __repr__ for their elements regardless of context:

print([Order("A-1", 10), Order("A-2", 20)])
# [<__main__.Order object at 0x7f...>, <__main__.Order object at 0x7f...>]

A list of ten of those in a log line is ten memory addresses and zero information.

Implementing both

class Order:
    def __init__(self, order_id, total):
        self.order_id = order_id
        self.total = total

    def __repr__(self):
        return f"Order(order_id={self.order_id!r}, total={self.total!r})"

    def __str__(self):
        return f"Order {self.order_id} for ${self.total:.2f}"
order = Order("A-1029", 49.99)

print(order)          # Order A-1029 for $49.99
print(repr(order))    # Order(order_id='A-1029', total=49.99)
print([order])        # [Order(order_id='A-1029', total=49.99)]
order                 # in the REPL: Order(order_id='A-1029', total=49.99)

Note !r inside the __repr__ f-string. It applies repr() to each field, so strings keep their quotes. Without it, Order(order_id=A-1029, ...) is ambiguous about whether the id is a string or a bare name — and that ambiguity is precisely what __repr__ exists to eliminate.

The fallback only runs one direction

This is the rule that decides which method to write first.

  • __str__ missing, __repr__ present → print() and str() use __repr__. Everything works.
  • __repr__ missing, __str__ present → print() looks fine, but the REPL, the debugger, containers, and %r formatting all show the default memory address.

So __repr__ alone gets you a working system. __str__ alone gets you a system that looks fine until you are actually debugging it, which is the worst possible time for the output to degrade.

Write __repr__ first. Add __str__ later, and only if the object is genuinely shown to end users and the developer form is wrong for them.

What each one should contain

The convention for __repr__ is that it should look like the code that would recreate the object. eval(repr(obj)) == obj is the aspiration — not enforced, often impossible, still the right instinct because it forces you to include the fields that matter.

When that is not achievable, use the angle-bracket form to signal it deliberately:

def __repr__(self):
    return f"<DBConnection host={self.host!r} pool={self.pool_size} open={self.is_open}>"

For __str__, drop the type name and the internals. It is for someone who does not care that this is a DBConnection instance.

One rule for both: never put a secret in either. A __repr__ that includes a password or an API key will eventually be written into a log, an error report, or a traceback that leaves your infrastructure. Mask it at the source.

def __repr__(self):
    return f"Credentials(user={self.user!r}, token='***')"

Dataclasses give you one for free

If the class is mostly a bag of fields, @dataclass writes a reasonable __repr__ for you.

from dataclasses import dataclass, field

@dataclass
class Order:
    order_id: str
    total: float
    token: str = field(repr=False)   # kept out of the repr

print(repr(Order("A-1029", 49.99, "secret")))
# Order(order_id='A-1029', total=49.99)

field(repr=False) is the built-in answer to the secrets problem. Use it on tokens, passwords, and anything large enough to make log lines unreadable — a cached response body does not belong in every traceback.

The generated __repr__ follows the recreatable-code convention, so you get the good default without writing it.

Where this pays off

The argument for spending five minutes on __repr__ is not tidiness. It is that logs and tracebacks are usually all you get.

When a background job fails in production, you are not attaching a debugger. You are reading whatever the process wrote before it stopped. If the traceback says processing failed for <Order object at 0x7f8b1c0d3fd0>, you know a thing failed. If it says processing failed for Order(order_id='A-1029', total=49.99), you can go look at that order.

Services on RunxBuild stream stdout and stderr into runtime logs you can read per deploy, so a good __repr__ is the difference between a log line that identifies the failing record and one that identifies a memory address that no longer exists. The infrastructure keeps the output; what is in it is your call.

How this fits the rest of the stack

Readable failures shorten incidents, and shorter incidents are cheaper — but log volume, runtime, storage, and the database all have their own numbers on the invoice. The RunxBuild hosting calculator shows those line items together so the monthly total is not a surprise.

Useful related references:

FAQ

What is the difference between str and repr in Python?

str is the human-readable form used by print() and str(). repr is the unambiguous developer form used by the REPL, debuggers, containers, and the %r format code. repr should ideally look like the code that would recreate the object.

Which should I implement if I only implement one?

repr. Python falls back to repr when str is missing, so implementing it covers both. The reverse is not true — implementing only str leaves the REPL, debugger, and any list containing your object printing memory addresses.

Why do my objects print as memory addresses inside a list?

Containers call repr on their elements, not str, regardless of whether you used print(). If you only implemented str, every element falls back to the default object.repr, which prints the class and address.

What does the !r conversion do in an f-string?

It applies repr() to the value instead of str(), so strings keep their quotes. Inside repr this removes ambiguity — order_id=‘A-1029’ is clearly a string, while order_id=A-1029 could be anything.

How do I keep secrets out of a repr?

Mask them explicitly, returning something like token=’***’ rather than the real value. In a dataclass, use field(repr=False) to exclude the attribute entirely. Reprs end up in logs and tracebacks, so anything in one should be safe to write down.

#python __str__#python __repr__#dunder methods#python classes#python debugging