Python __repr__ vs __str__: String Representations
Every Python object can be converted to a string. When you print an object or inspect it in the REPL, Python calls special methods to determine what text to display. Without custom implementations,...
Key Insights
__repr__()targets developers and should provide unambiguous, ideally reproducible representations, while__str__()targets end-users with readable output- When
__str__()is undefined, Python falls back to__repr__(), making__repr__()the more critical method to implement - Always implement
__repr__()for debugging and logging; add__str__()only when you need distinctly different user-facing output
Why String Representations Matter
Every Python object can be converted to a string. When you print an object or inspect it in the REPL, Python calls special methods to determine what text to display. Without custom implementations, you get unhelpful default output that looks like <__main__.User object at 0x7f8b4c3d2e80>.
Understanding __repr__() and __str__() transforms debugging from frustrating to efficient. These methods control how your objects appear in logs, error messages, and interactive sessions. The difference between the two isn’t arbitrary—each serves a distinct audience with different needs.
class User:
def __init__(self, name, user_id):
self.name = name
self.user_id = user_id
user = User("Alice", 12345)
print(user) # <__main__.User object at 0x7f8b4c3d2e80>
print(repr(user)) # <__main__.User object at 0x7f8b4c3d2e80>
This default output tells you nothing about the object’s state. Let’s fix that.
Understanding __str__(): Human-Readable Output
The __str__() method creates human-readable representations. Python calls it when you use str() or print(). Think of it as the “pretty” version meant for end-users who don’t care about technical details.
Implement __str__() when your object has a natural, user-friendly representation. For a User object, that might be just the name. For a Product, it could be the title and price. The goal is clarity, not completeness.
class Product:
def __init__(self, name, price, sku):
self.name = name
self.price = price
self.sku = sku
def __str__(self):
return f"{self.name} - ${self.price:.2f}"
product = Product("Wireless Mouse", 29.99, "WM-2024")
print(product) # Wireless Mouse - $29.99
print(f"Available: {product}") # Available: Wireless Mouse - $29.99
Notice how __str__() omits the SKU. End-users don’t need internal identifiers cluttering their view. This representation works perfectly for receipts, UI displays, or customer-facing messages.
When should you skip __str__()? If there’s no meaningful user-facing representation distinct from the developer representation, don’t implement it. Simple data containers rarely need both methods.
Understanding __repr__(): Unambiguous Developer Output
The __repr__() method targets developers. Python calls it via repr(), in the interactive interpreter, and inside containers like lists. The official Python documentation states that __repr__() should return a string that, when passed to eval(), recreates the object. While not always achievable, this principle guides good implementations.
The critical behavior: if __str__() is undefined, Python uses __repr__() as a fallback. This makes __repr__() more fundamental—implement it first, always.
class User:
def __init__(self, name, user_id, email):
self.name = name
self.user_id = user_id
self.email = email
def __repr__(self):
return f"User(name={self.name!r}, user_id={self.user_id!r}, email={self.email!r})"
user = User("Bob", 67890, "bob@example.com")
print(repr(user)) # User(name='Bob', user_id=67890, email='bob@example.com')
# In interactive interpreter
user # User(name='Bob', user_id=67890, email='bob@example.com')
This representation is unambiguous. You can see exactly what values the object contains. In many cases, you could copy this string, paste it into code, and recreate the object (assuming the class is imported).
The !r format specifier in the f-string calls repr() on each value, ensuring strings get quotes. Without it, name=Bob looks like a variable reference rather than the string "Bob".
Key Differences and When to Use Each
Here’s the practical breakdown:
| Aspect | __str__() |
__repr__() |
|---|---|---|
| Audience | End-users | Developers |
| Called by | str(), print(), format() |
repr(), REPL, containers |
| Goal | Readability | Unambiguity |
| Fallback | Uses __repr__() if undefined |
Uses default if undefined |
| Convention | Human-friendly | Eval-able when possible |
Implement both when your object has distinctly different user and developer representations. Implement only __repr__() for internal classes, data structures, and debugging-focused objects. Rarely implement only __str__()—if you’re adding string conversion, start with __repr__().
class Transaction:
def __init__(self, amount, currency, timestamp):
self.amount = amount
self.currency = currency
self.timestamp = timestamp
def __repr__(self):
return (f"Transaction(amount={self.amount!r}, "
f"currency={self.currency!r}, timestamp={self.timestamp!r})")
def __str__(self):
return f"{self.amount} {self.currency} at {self.timestamp.strftime('%Y-%m-%d %H:%M')}"
from datetime import datetime
txn = Transaction(150.00, "USD", datetime(2024, 1, 15, 14, 30))
print(txn) # 150.0 USD at 2024-01-15 14:30
print(repr(txn)) # Transaction(amount=150.0, currency='USD', timestamp=datetime.datetime(2024, 1, 15, 14, 30))
# In containers, __repr__() is used
transactions = [txn]
print(transactions) # [Transaction(amount=150.0, currency='USD', timestamp=datetime.datetime(2024, 1, 15, 14, 30))]
Notice how the list uses __repr__() even though print() normally calls __str__(). Containers always use __repr__() for their items to maintain unambiguous output.
Best Practices and Common Patterns
Always use !r in your __repr__() f-strings. This ensures nested objects display correctly and strings include quotes. Compare these approaches:
class Address:
def __init__(self, street, city):
self.street = street
self.city = city
def __repr__(self):
return f"Address(street={self.street!r}, city={self.city!r})"
class Person:
def __init__(self, name, address):
self.name = name
self.address = address
def __repr__(self):
return f"Person(name={self.name!r}, address={self.address!r})"
addr = Address("123 Main St", "Springfield")
person = Person("Charlie", addr)
print(repr(person))
# Person(name='Charlie', address=Address(street='123 Main St', city='Springfield'))
# The representation is eval-able (if classes are imported)
from datetime import datetime
reconstructed = eval(repr(person))
print(reconstructed.address.street) # 123 Main St
The !r specifier cascades through nested objects, each calling its own __repr__(). This creates deeply informative output that reveals your entire object graph.
For data-heavy classes, Python’s dataclasses module auto-generates excellent __repr__() implementations:
from dataclasses import dataclass
@dataclass
class Config:
host: str
port: int
debug: bool = False
config = Config("localhost", 8080, True)
print(repr(config)) # Config(host='localhost', port=8080, debug=True)
This follows all best practices automatically: unambiguous, eval-able, uses !r formatting.
Real-World Application: Debugging and Logging
Proper string representations dramatically improve debugging. Consider this logging scenario:
import logging
logging.basicConfig(level=logging.INFO)
class APIRequest:
def __init__(self, method, endpoint, params):
self.method = method
self.endpoint = endpoint
self.params = params
def __repr__(self):
return (f"APIRequest(method={self.method!r}, "
f"endpoint={self.endpoint!r}, params={self.params!r})")
def execute(self):
logging.info(f"Executing: {self!r}")
# API call logic here
pass
request = APIRequest("GET", "/api/users", {"page": 1, "limit": 10})
request.execute()
# INFO:root:Executing: APIRequest(method='GET', endpoint='/api/users', params={'page': 1, 'limit': 10})
That log message tells you everything. Without __repr__(), you’d see APIRequest object at 0x... and have no idea what request failed. In production systems with thousands of log lines, this difference means finding bugs in minutes versus hours.
When exceptions occur, __repr__() appears in tracebacks. Clear representations help you understand the state that caused the error:
class DatabaseConnection:
def __init__(self, host, port, database):
self.host = host
self.port = port
self.database = database
def __repr__(self):
return f"DatabaseConnection(host={self.host!r}, port={self.port!r}, database={self.database!r})"
def connect(self):
if self.port < 1024:
raise ValueError(f"Invalid port for {self!r}")
conn = DatabaseConnection("localhost", 80, "myapp")
conn.connect()
# ValueError: Invalid port for DatabaseConnection(host='localhost', port=80, database='myapp')
The error message shows exactly which connection failed, with all relevant details.
Quick Reference and Decision Guide
Use this template for most classes:
class MyClass:
def __init__(self, arg1, arg2):
self.arg1 = arg1
self.arg2 = arg2
def __repr__(self):
return f"{self.__class__.__name__}(arg1={self.arg1!r}, arg2={self.arg2!r})"
def __str__(self):
# Only implement if you need different user-facing output
return f"MyClass with {self.arg1}"
Decision flowchart:
- Always implement
__repr__()for any class you’ll debug or log - Add
__str__()only if end-users need a simplified view - Use
!rin__repr__()f-strings for proper nesting - Make
__repr__()eval-able when practical, but don’t sacrifice clarity - Consider
@dataclassfor simple data containers
The investment in proper string representations pays off immediately. Your logs become readable, your debugging sessions become productive, and your code becomes more maintainable. These two methods are among the most impactful dunder methods you can implement—use them well.