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:

  1. Always implement __repr__() for any class you’ll debug or log
  2. Add __str__() only if end-users need a simplified view
  3. Use !r in __repr__() f-strings for proper nesting
  4. Make __repr__() eval-able when practical, but don’t sacrifice clarity
  5. Consider @dataclass for 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.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.