Learning Goals
3 minBy the end of this lesson you can:
- Define
__str__to control whatprint(obj)andstr(obj)show. - Define
__repr__to control what the REPL andrepr(obj)show. - Explain why these are different — one for users, one for debugging.
- Recognise
__repr__as the fallback for__str__if you only write one.
Warm-Up · The Ugly Default
5 minclass Hero: def __init__(self, name, hp): self.name = name; self.hp = hp h = Hero("Aisyah", 80) print(h) # <__main__.Hero object at 0x7f8b...> print([h, h]) # [<...>, <...>] str(h) # same useless string
Python falls back to a default that includes the class name and memory address. Useless for anyone who isn't a Python implementer.
Two dunders control how your object becomes a string. __str__ is for end-users (pretty, friendly). __repr__ is for developers (precise, ideally re-creatable with eval).
New Concept · The Two Audiences
14 min__str__ — for users
class Hero: def __init__(self, name, hp): self.name = name; self.hp = hp def __str__(self): return f"{self.name} ({self.hp} HP)" h = Hero("Aisyah", 80) print(h) # → Aisyah (80 HP) str(h) # → 'Aisyah (80 HP)'
Pretty. Human-readable. print calls str() internally, which calls __str__.
__repr__ — for developers
Best practice: __repr__ should look like the code that constructs the object — if you typed it back, you'd get a fresh equivalent instance.
class Hero: def __init__(self, name, hp): self.name = name; self.hp = hp def __repr__(self): return f"Hero(name={self.name!r}, hp={self.hp})" h = Hero("Aisyah", 80) h # in REPL → Hero(name='Aisyah', hp=80) repr(h) # → "Hero(name='Aisyah', hp=80)"
The REPL (typing h and hitting Enter) shows __repr__. print shows __str__. They're different.
The fallback
If you only define __repr__, Python uses it for both. If you only define __str__, __repr__ falls back to the ugly default.
Rule of thumb: always define __repr__. Define __str__ only if it should differ from __repr__.
Inside containers · __repr__ wins
When your object is inside a list, dict, set, etc., printing the container shows the repr of each item:
heroes = [Hero("Aisyah", 80), Hero("Wei Jie", 50)] print(heroes) # → [Hero(name='Aisyah', hp=80), Hero(name='Wei Jie', hp=50)] # ^^ uses __repr__ of each, NOT __str__
That's why __repr__ matters even when you have a __str__. Debug printing of containers depends on it.
The !r conversion in f-strings
In an f-string, {x!r} calls repr(x) on the value before inserting. {x} calls str(x). Use !r when you want quotes around strings (typical in __repr__ implementations).
name = "Aisyah" print(f"Hello {name}") # → Hello Aisyah print(f"Hello {name!r}") # → Hello 'Aisyah'
The contract
__str__ __repr__
print(x) typing x in REPL
str(x) repr(x)
f"{x}" f"{x!r}"
when x is inside a list/dict/tuple
Goal: Goal:
pretty precise + ideally re-creatable
human developerWorked Example · A Vector Class
12 minVectors are a classic dunder example — clean syntax, easy maths. Save as vector.py:
# vector.py — Vector with __str__ and __repr__ class Vector: def __init__(self, x, y): self.x = x self.y = y def __str__(self): return f"({self.x}, {self.y})" def __repr__(self): return f"Vector(x={self.x}, y={self.y})" v = Vector(3, 4) print(v) # pretty — for users print(repr(v)) # debug — for developers print(f"point: {v}") # uses __str__ print(f"debug: {v!r}")# uses __repr__ # Inside a container points = [Vector(0, 0), Vector(3, 4), Vector(-2, 5)] print(points) # uses __repr__ of each
Output
(3, 4) Vector(x=3, y=4) point: (3, 4) debug: Vector(x=3, y=4) [Vector(x=0, y=0), Vector(x=3, y=4), Vector(x=-2, y=5)]
Read the diff
Two methods, two distinct outputs for two distinct audiences. The user sees (3, 4). The developer sees Vector(x=3, y=4) — which they could literally paste back into Python to recreate the vector. The list of vectors uses __repr__ for each element, because that's the convention containers follow.
If you paste Vector(x=3, y=4) back into Python, you get a working Vector. That's the goal of a good __repr__ — exact, complete, runnable. When that's not feasible (because the class has private state, or non-trivial construction), use <Vector x=3 y=4> style instead — clearly debug output, no eval ambiguity.
Try It Yourself
13 minAdd __str__ to Book that returns "Title by Author (year)". Add __repr__ that returns Book(title='...', author='...', year=...). Test both.
Hint
class Book: def __init__(self, title, author, year): self.title = title; self.author = author; self.year = year def __str__(self): return f"{self.title} by {self.author} ({self.year})" def __repr__(self): return f"Book(title={self.title!r}, author={self.author!r}, year={self.year})" b = Book("Charlotte's Web", "White", 1952) print(b) # Charlotte's Web by White (1952) print(repr(b)) # Book(title="Charlotte's Web", author='White', year=1952) print([b]) # uses __repr__
Notice the !r on string fields in __repr__ — gives proper quote-escaping for free.
Define a class with only __repr__. Confirm print(obj) uses it. Then define a class with only __str__. Confirm a list of those still shows the ugly default.
Hint
class ReprOnly: def __repr__(self): return "ReprOnly()" class StrOnly: def __str__(self): return "I have a str" print(ReprOnly()) # → ReprOnly() print(StrOnly()) # → I have a str print([StrOnly()]) # → [<__main__.StrOnly object at 0x...>] -- ugly!
That's why "always define __repr__" is the rule. The container print is the case that catches people out.
Define Point(x, y, label) with a re-creatable __repr__. Then run p2 = eval(repr(p1)) and confirm p2 is an equivalent Point.
Hint
class Point: def __init__(self, x, y, label): self.x = x; self.y = y; self.label = label def __repr__(self): return f"Point(x={self.x}, y={self.y}, label={self.label!r})" p1 = Point(3, 4, "origin") print(repr(p1)) # → Point(x=3, y=4, label='origin') p2 = eval(repr(p1)) # WARNING: eval is dangerous; only for trusted input print(p2.label) # → origin
Never use eval on input from outside your program — it'll happily run any Python code, including __import__('os').system('rm -rf /'). But for a developer's own debug roundtrip, it's fine.
Mini-Challenge · The Repr-Friendly Inventory
8 minBuild inventory.py. Define Item(name, price, qty) with both dunders. __str__ shows it like a receipt line. __repr__ shows constructor form. Then build a list of 5 items and:
printeach item — uses__str__.print(items)— uses__repr__on each.- Print a sum of all prices.
Show one possible solution
# inventory.py — Item with both dunders class Item: def __init__(self, name, price, qty): self.name = name; self.price = price; self.qty = qty def __str__(self): return f"{self.name:<14} x{self.qty:<3} RM {self.price:>6.2f}" def __repr__(self): return f"Item(name={self.name!r}, price={self.price}, qty={self.qty})" items = [ Item("nasi lemak", 8.0, 1), Item("teh tarik", 3.5, 2), Item("roti planta", 4.5, 1), Item("cendol", 5.0, 3), Item("milo", 4.5, 1), ] print("=== Receipt ===") for it in items: print(it) # __str__ — pretty print() print("Debug:", items) # __repr__ on each — precise print(f"Total: RM {sum(it.price * it.qty for it in items):.2f}")
Non-negotiables: both dunders defined, the receipt uses str, the debug print uses repr. Notice print(items) auto-uses the repr — that's the container behaviour.
Recap
3 minTwo dunders, two audiences. __str__ for users — pretty, readable. __repr__ for developers — precise, ideally re-creatable. print(obj) calls __str__; the REPL and containers call __repr__. Use !r in f-strings to call repr. Always define __repr__; define __str__ only if it should differ. These are the first of many dunders we'll meet — every Python operator and builtin has a dunder behind it.
Vocabulary Card
- __str__
- Pretty string for end-users. Called by
print,str(),f"{x}". - __repr__
- Precise string for developers. Called by REPL,
repr(), containers,f"{x!r}". - !r conversion
- Inside an f-string,
{x!r}callsrepr(x)on the value. - re-creatable repr
- A
__repr__that, when pasted back into Python, constructs an equivalent object.
Homework
4 minTake your Hero class from earlier in Level 3. Add both __str__ and __repr__. The str should look like a status banner (one line, HP bar, level). The repr should be the constructor call.
Test that print(hero) shows the banner. Test that print([h1, h2]) shows the constructor reprs. Test that f"debug: {hero!r}" shows the repr.
Sample · Hero dunders
class Hero: def __init__(self, name, hp=100, attack_power=12, level=1): self.name = name self.hp = hp; self.max_hp = hp self.attack_power = attack_power self.level = level def __str__(self): bar = "█" * (self.hp * 10 // self.max_hp) + "░" * (10 - self.hp * 10 // self.max_hp) return f"{self.name} (Lv {self.level}) HP {self.hp}/{self.max_hp} [{bar}]" def __repr__(self): return (f"Hero(name={self.name!r}, hp={self.hp}, " f"attack_power={self.attack_power}, level={self.level})") a = Hero("Aisyah", 80) b = Hero("Wei Jie", 60, attack_power=15) print(a) # banner — __str__ print(b) print([a, b]) # constructor reprs — __repr__ print(f"debug: {a!r}") # explicit !r
Non-negotiables: both dunders, str shows an HP bar, repr is the constructor call. The bar uses string multiplication from L1.