Display: repr and str
In Chapter 59, James built the SmartNotes hierarchy: TextNote, CodeNote, LinkNote, and NoteCollection. Fifteen tests, all green. Now he wants to see his work.
He creates a note and prints it:
note = Note("Meeting Notes", "Discussed Q3 roadmap", 4)
print(note)
Output:
<__main__.Note object at 0x104a8b2d0>
"That is the memory address," Emma says. "Python asked your object for a string. Your object said: 'I am a Note, and I live at this address.' Not exactly helpful."
"How do I make it show the title?"
"You write __str__. And before that, you write __repr__. Two methods, two audiences."
Same cycle, same tools. When you specify special methods, you are specifying what built-in operations return for your class. Your /tdg command works on special methods too.
A special method (also called a dunder method, short for "double underscore") is a method whose name starts and ends with __. Python calls these methods automatically when you use built-in operations. print(note) calls note.__str__(). repr(note) calls note.__repr__(). You write the method; Python calls it.
Python's data model uses __dunder__ methods as hooks into the language runtime. __repr__ should return an unambiguous string (ideally eval()-able). __str__ should return a human-readable string. If only __repr__ is defined, str() and print() fall back to it. If only __str__ is defined, repr() still shows the default <ClassName object at 0x...>.
The Default Display Problem
Create a file called display_demo.py:
class Note:
def __init__(self, title: str, body: str, word_count: int) -> None:
self.title = title
self.body = body
self.word_count = word_count
note = Note("Meeting Notes", "Discussed Q3 roadmap", 4)
print(note)
print(repr(note))
Run it:
uv run python display_demo.py
Output:
<__main__.Note object at 0x104a8b2d0>
<__main__.Note object at 0x104a8b2d0>
Both print() and repr() show the same unhelpful string. Python's default __repr__ shows the class name and memory address. Python's default __str__ falls back to __repr__. Neither shows the data inside the object.
Think of it this way: every product in James's warehouse has two labels. The barcode is for machines and the warehouse system -- it uniquely identifies the product with a scannable code. The display label is for customers -- it shows the product name, size, and price in readable text. Right now, the Note has neither label. It only has a serial number (the memory address).
repr: The Barcode
__repr__ is the developer representation. It answers the question: what exactly is this object? The convention is to return a string that looks like the constructor call that would recreate the object.
Add __repr__ to the class:
class Note:
def __init__(self, title: str, body: str, word_count: int) -> None:
self.title = title
self.body = body
self.word_count = word_count
def __repr__(self) -> str:
return f"Note(title={self.title!r}, word_count={self.word_count})"
note = Note("Meeting Notes", "Discussed Q3 roadmap", 4)
print(repr(note))
Output:
Note(title='Meeting Notes', word_count=4)
Three things to notice:
-
self.title!ruses the!rformat specifier, which puts quotes around strings. Without it,Meeting Noteswould appear without quotes and you could not tell whether the value is a string or a variable name. -
The format looks like a constructor call. A developer reading
Note(title='Meeting Notes', word_count=4)immediately knows the type and key values. This is the convention, not a requirement. -
bodyis excluded. The body could be thousands of words.__repr__should be concise. Include the fields that identify the object; exclude the fields that would bury the useful information.
str: The Display Label
__str__ is the human-friendly representation. It answers the question: how should this object introduce itself?
Add __str__ to the class:
class Note:
def __init__(self, title: str, body: str, word_count: int,
is_draft: bool = True) -> None:
self.title = title
self.body = body
self.word_count = word_count
self.is_draft = is_draft
def __repr__(self) -> str:
return f"Note(title={self.title!r}, word_count={self.word_count})"
def __str__(self) -> str:
status = "draft" if self.is_draft else "published"
return f"{self.title} ({self.word_count} words, {status})"
note = Note("Meeting Notes", "Discussed Q3 roadmap", 4)
print(repr(note))
print(str(note))
print(note)
Output:
Note(title='Meeting Notes', word_count=4)
Meeting Notes (4 words, draft)
Meeting Notes (4 words, draft)
repr() shows the developer view. str() and print() show the human view. Two labels, two audiences.
When Python Calls Which
This is the part most developers get wrong. Python does not always call the method you expect:
| Operation | Method Called | Purpose |
|---|---|---|
repr(note) | __repr__ | Developer/debugging view |
str(note) | __str__ | Human-readable view |
print(note) | __str__ | Same as str() |
f"{note}" | __str__ | Same as str() |
f"{note!r}" | __repr__ | Force developer view in f-string |
[note] in REPL | __repr__ on each element | Lists show repr of contents |
The critical rule: when you put objects in a container (list, dict, set), Python uses __repr__ to display them, not __str__.
notes = [
Note("Meeting Notes", "Q3 roadmap", 4),
Note("Design Doc", "Architecture plan", 12),
]
print(notes)
Output:
[Note(title='Meeting Notes', word_count=4), Note(title='Design Doc', word_count=12)]
The list shows __repr__ for each element, even though you called print(). This is why __repr__ is more important than __str__: it appears in more places.
The Fallback Rule
If you define only __repr__ and skip __str__, then str() and print() fall back to __repr__:
class Note:
def __init__(self, title: str, word_count: int) -> None:
self.title = title
self.word_count = word_count
def __repr__(self) -> str:
return f"Note(title={self.title!r}, word_count={self.word_count})"
note = Note("Meeting Notes", 4)
print(repr(note)) # Note(title='Meeting Notes', word_count=4)
print(str(note)) # Note(title='Meeting Notes', word_count=4) ← fallback
print(note) # Note(title='Meeting Notes', word_count=4) ← fallback
Output:
Note(title='Meeting Notes', word_count=4)
Note(title='Meeting Notes', word_count=4)
Note(title='Meeting Notes', word_count=4)
If you define only __str__ and skip __repr__, the fallback does not work in reverse:
class Note:
def __init__(self, title: str, word_count: int) -> None:
self.title = title
self.word_count = word_count
def __str__(self) -> str:
return f"{self.title} ({self.word_count} words)"
note = Note("Meeting Notes", 4)
print(str(note)) # Meeting Notes (4 words) ← uses __str__
print(repr(note)) # <__main__.Note object at 0x...> ← default!
Output:
Meeting Notes (4 words)
<__main__.Note object at 0x104a8b2d0>
Rule: Always implement __repr__ first. It is the foundation. __str__ is optional and only needed when you want a different human-readable format.
PRIMM-AI+ Practice: Display Methods
Predict [AI-FREE]
Press Shift+Tab to enter Plan Mode before predicting.
Given this class:
class Product:
def __init__(self, name: str, price: float) -> None:
self.name = name
self.price = price
def __repr__(self) -> str:
return f"Product(name={self.name!r}, price={self.price})"
def __str__(self) -> str:
return f"{self.name}: ${self.price:.2f}"
p = Product("Widget", 9.99)
Predict the output for each line. Write your prediction and a confidence score from 1 to 5.
print(p)print(repr(p))print([p])print(f"Item: {p}")print(f"Debug: {p!r}")
Check your predictions
Widget: $9.99--print()calls__str__Product(name='Widget', price=9.99)--repr()calls__repr__[Product(name='Widget', price=9.99)]-- lists use__repr__for elementsItem: Widget: $9.99-- f-strings call__str__by defaultDebug: Product(name='Widget', price=9.99)--!rforces__repr__in f-strings
Run
Press Shift+Tab to exit Plan Mode.
Create display_practice.py with the Product class above. Add all five print statements and run:
uv run python display_practice.py
Compare each output to your prediction. Pay special attention to line 3 (the list case).
Investigate
In Claude Code, type:
/investigate @display_practice.py
Ask: "If I remove __str__ from this class, which print statements change their output and which stay the same? Why?"
Try With AI
If Claude Code is not already running, open your terminal, navigate to your SmartNotes project folder, and type claude. If you need a refresher, Chapter 44 covers the setup.
Prompt 1: Implement repr for SmartNotes
Here is my Note class from Chapter 58:
class Note:
def __init__(self, title: str, body: str, word_count: int,
author: str = "Anonymous", is_draft: bool = True,
tags: list[str] | None = None) -> None:
self.title = title
self.body = body
self.word_count = word_count
self.author = author
self.is_draft = is_draft
self.tags = tags if tags is not None else []
Write __repr__ for this class. The repr should include
title, word_count, and author but NOT body (it could be
very long). Explain why you chose those fields.
Read the AI's implementation. Verify that it uses !r for string fields and returns a format that looks like a constructor call.
What you're learning: You are seeing how to select which fields to include in __repr__. The AI explains the trade-off between completeness (showing everything) and readability (showing only identifying information). You evaluate whether its field selection matches your own reasoning.
Prompt 2: Debug a str Problem
I have this class and I am confused about the output:
class Task:
def __init__(self, name: str, done: bool = False) -> None:
self.name = name
self.done = done
def __str__(self) -> str:
status = "done" if self.done else "pending"
return f"{self.name} [{status}]"
tasks = [Task("Write report"), Task("Send email", True)]
print(tasks)
The output shows memory addresses instead of my nice
format. Why? How do I fix it?
What you're learning: You are learning the critical distinction between __str__ and __repr__. The AI explains that lists use __repr__ for their elements. To fix the output, you need to add __repr__, not modify __str__. This reinforces the "always implement __repr__ first" rule.
Prompt 3: Display Methods for Your Domain
I work in [describe your professional domain: logistics,
healthcare, education, finance, etc.]. Design a class
for a common object in my domain with both __repr__ and
__str__. Show me what repr() and print() would output.
Explain your field choices for each method.
What you're learning: You are transferring the __repr__/__str__ pattern from SmartNotes to your own domain. The AI generates a class relevant to your work, and you evaluate whether its field selection for __repr__ (identifying fields) and __str__ (readable summary) matches the guidelines from this lesson.
James looks at his Note. repr(note) shows Note(title='Meeting Notes', word_count=4). print(note) shows Meeting Notes (4 words, draft). Two views of the same object.
"This is like product labels in the warehouse," James says. "The barcode goes on the back of the package -- it is for the scanning system. The display label goes on the front -- it is for the customer picking items off the shelf. Same product, two audiences."
Emma nods. "And here is where I messed up on a real project. I implemented __str__ but not __repr__. Everything looked fine when I used print(). Then I put objects in a list and printed the list. Memory addresses everywhere. Spent thirty minutes adding print loops before I realized I needed __repr__, not more print statements. That is why the rule is: __repr__ first, always."
"What about __eq__?" James asks. "I created two notes with the same title and body. note1 == note2 returns False."
"That is the next lesson. Your objects can display themselves now. They cannot compare themselves yet."