Discover why Python's __str__, __repr__, and __format__ methods are more than just fancy print tricks. Learn how they impact debugging, logging, testing, and even user interfaces with real-world examples and hidden tips that most developers overlook.
If you've ever worked with Python and printed an object only to see something like <MyClass object at 0x102b4a310>
, you're not alone. That kind of output is the default behavior when Python doesn't know how to turn your object into a meaningful string. Most people shrug it off. But here’s the thing:
Python’s string representation methods (
__str__
,__repr__
, and__format__
) are low-effort, high-impact tools that can drastically improve the quality of your code, especially for debugging, logging, and building user-facing tools.
Let’s break down what they are, why they matter, and how to actually use them right.
Python has three core string conversion hooks in classes:
Method | What It's For | Called When... |
---|---|---|
__str__ |
For humans (readable) | print(obj) or str(obj) |
__repr__ |
For devs (unambiguous, debug) | Shell/REPL display, logging, containers |
__format__ |
For custom formatting logic | format(obj) , f"{obj:spec}" |
Let’s go beyond the basics and explore where each of these really shows up and how to master them.
This is what users see when you print the object.
class User:
def __init__(self, name, email):
self.name = name
self.email = email
def __str__(self):
return f"{self.name} <{self.email}>"
user = User("Alice", "alice@example.com")
print(user)
# Output: Alice <alice@example.com>
That looks much better than <User object at 0xABC123>
.
Integrates With f-strings
user = User("Bob", "bob@example.com")
print(f"User: {user}") # Uses __str__ automatically
If __str__
isn't defined, Python falls back to __repr__
. If that's not defined either, you get the ugly memory address thing.
This should return a developer-readable version of the object. Ideally, something that could recreate the object if passed to eval()
when possible.
class User:
def __init__(self, name, email):
self.name = name
self.email = email
def __repr__(self):
return f"User(name={self.name!r}, email={self.email!r})"
print([User("Alice", "a@example.com"), User("Bob", "b@example.com")])
# Output: [User(name='Alice', email='a@example.com'), User(name='Bob', email='b@example.com')]
Notice the use of !r
. It's shorthand for using repr()
inside f-strings.
This is the lesser-known star of the show. It's used when formatting with str.format()
or f-strings with format specifiers.
class Price:
def __init__(self, amount):
self.amount = amount
def __format__(self, spec):
if spec == "euro":
return f"\\u20ac{self.amount:.2f}"
elif spec == "usd":
return f"${self.amount:.2f}"
return f"{self.amount:.2f}"
p = Price(19.99)
print(f"Price: {p:euro}") # €19.99
print(f"Price: {p:usd}") # $19.99
Combine __format__
with locale
for internationalized output. It also works great in reporting tools or APIs where you want different string views.
def __str__(self):
return 123 # TypeError!
Always return strings, not numbers or None.
They have different jobs. Don’t make them twins unless your object is dead simple.
def __str__(self):
return str(self) # Infinite recursion!
Use self.attribute
instead.
Scenario: You're building a Django or FastAPI backend and want better logging for your UserSession
objects.
class UserSession:
def __init__(self, user_id, ip_address, active):
self.user_id = user_id
self.ip_address = ip_address
self.active = active
def __repr__(self):
return (f"UserSession(user_id={self.user_id!r}, "
f"ip_address={self.ip_address!r}, active={self.active})")
Why it matters:
session = UserSession(42, "192.168.0.1", True)
# Logs will show this:
print(session)
# Output: UserSession(user_id=42, ip_address='192.168.0.1', active=True)
This avoids ambiguity and shows the dev-friendly internal state, useful for bug tracing.
Scenario: You’re building a CLI tool that lists files or reports.
class Report:
def __init__(self, name, status):
self.name = name
self.status = status
def __str__(self):
return f"[{self.status.upper()}] {self.name}"
Usage:
report = Report("Q2 Financial Summary", "ok")
print(report)
# Output: [OK] Q2 Financial Summary
Clear, readable output for end-users. If the user doesn’t care about internals, __str__
hides them elegantly.
Scenario: You're building a financial dashboard where amounts need to be shown in various currencies.
class Money:
def __init__(self, amount):
self.amount = amount
def __format__(self, spec):
if spec == 'usd':
return f"${self.amount:,.2f}"
elif spec == 'eur':
return f"€{self.amount:,.2f}"
elif spec == 'btc':
return f"{self.amount:.6f} BTC"
return f"{self.amount:.2f}" # Default format
Usage:
m = Money(15432.75)
print(f"Price: {m:usd}") # Output: Price: $15,432.75
print(f"Price: {m:eur}") # Output: Price: €15,432.75
print(f"Price: {m:btc}") # Output: Price: 15432.750000 BTC
Dynamic formatting lets you use the same object in multiple UI contexts, based on format specifiers.
Scenario: You're writing tests using pytest
or unittest
.
class Product:
def __init__(self, id, name):
self.id = id
self.name = name
def __repr__(self):
return f"Product(id={self.id!r}, name={self.name!r})"
Test output:
assert Product(1, "Banana") == Product(2, "Apple")
# Output from test framework:
# E AssertionError: assert Product(id=1, name='Banana') == Product(id=2, name='Apple')
Without __repr__
, you’d just see memory addresses — which helps no one.
Scenario: You’re building custom objects to analyze tabular data and use them inside Jupyter notebooks.
class DataPoint:
def __init__(self, label, value):
self.label = label
self.value = value
def __repr__(self):
return f"<{self.label}: {self.value}>"
data = [DataPoint("Temperature", 21.5), DataPoint("Humidity", 60)]
data # In Jupyter you'll see a list with readable reprs
Without this, Jupyter just shows a raw list of object memory locations.
class HTMLUser:
def __init__(self, name, role):
self.name = name
self.role = role
def _repr_html_(self):
return f\\"\\"\\"<b>{self.name}</b> - <i>{self.role}</i>\\"\\"\\"
_repr_html_()
isn’t technically part of __str__
/__repr__
, but it's closely related. It allows Jupyter to display rich previews.
Create a reusable ReprMixin
to auto-generate a good __repr__
:
class ReprMixin:
def __repr__(self):
attrs = ', '.join(f"{k}={v!r}" for k, v in self.__dict__.items())
return f"{self.__class__.__name__}({attrs})"
class Product(ReprMixin):
def __init__(self, id, title):
self.id = id
self.title = title
print(Product(10, "Banana"))
# Output: Product(id=10, title='Banana')
String representation methods aren’t just fluff. They are your objects' public voice. They control how readable, debuggable, and intuitive your system is.
Whether you’re building a library, a web API, a machine learning pipeline, or a CLI tool, spending a few extra minutes designing your __str__
, __repr__
, and __format__
methods will pay off in clarity, ease of debugging, and polish.
__str__
for people__repr__
for devs__format__
for customizationAnd never underestimate their power again.