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.
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
- Implementing both
- The fallback only runs one direction
- What each one should contain
- Dataclasses give you one for free
- Where this pays off
- How this fits the rest of the stack
- FAQ
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()andstr()use__repr__. Everything works.__repr__missing,__str__present →print()looks fine, but the REPL, the debugger, containers, and%rformatting 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:
- Python str vs repr: Get This Wrong and Your Logs Are Useless
- Python Not Equal: != vs is not, and Why the Difference Bites
- Python Integer Division: Why // Floors and Why -7 // 2 Is -4
- Python services on RunxBuild
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.