DEV Community

Heval Hazal Kurt
Heval Hazal Kurt

Posted on

Behind the Underscores EP03: String Representation Methods (__str__, __repr__, __format__)

Behind the Underscores EP03: String Representation Methods

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.

What Are These Methods, Really?

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.

__str__: A Friendly Face for Your Object

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>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

If __str__ isn't defined, Python falls back to __repr__. If that's not defined either, you get the ugly memory address thing.

__repr__: For Debugging and Developers

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')]
Enter fullscreen mode Exit fullscreen mode

Notice the use of !r . It's shorthand for using repr() inside f-strings.

Why __repr__ Is Crucial:

  • Logging systems use it
  • Lists and dicts use it for elements
  • Your REPL/shell relies on it
  • Debuggers and tracebacks show it

__format__: Your Object on the Runway

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
Enter fullscreen mode Exit fullscreen mode

Tip:

Combine __format__ with locale for internationalized output. It also works great in reporting tools or APIs where you want different string views.

Mistakes to Avoid

Returning Non-Strings

def __str__(self):
    return 123  # TypeError!
Enter fullscreen mode Exit fullscreen mode

Always return strings, not numbers or None.

Making __str__ and __repr__ Identical

They have different jobs. Don’t make them twins unless your object is dead simple.

Calling str(self) Inside __str__

def __str__(self):
    return str(self)  # Infinite recursion!
Enter fullscreen mode Exit fullscreen mode

Use self.attribute instead.

Real-World Places Where These Matter

1. Logging and Debugging in a Web App (Using __repr__)

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})")
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

This avoids ambiguity and shows the dev-friendly internal state, useful for bug tracing.

Displaying Clean Info in a CLI App (Using __str__)

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}"
Enter fullscreen mode Exit fullscreen mode

Usage:

report = Report("Q2 Financial Summary", "ok")
print(report)

# Output: [OK] Q2 Financial Summary
Enter fullscreen mode Exit fullscreen mode

Clear, readable output for end-users. If the user doesn’t care about internals, __str__ hides them elegantly.

Multi-Currency Display in a Finance App (Using __format__)

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Dynamic formatting lets you use the same object in multiple UI contexts, based on format specifiers.

Cleaner Test Failures with __repr__

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})"
Enter fullscreen mode Exit fullscreen mode

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')
Enter fullscreen mode Exit fullscreen mode

Without __repr__, you’d just see memory addresses — which helps no one.

Use in Pandas or Jupyter for Previewing Data Objects

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}>"
Enter fullscreen mode Exit fullscreen mode
data = [DataPoint("Temperature", 21.5), DataPoint("Humidity", 60)]
data  # In Jupyter you'll see a list with readable reprs
Enter fullscreen mode Exit fullscreen mode

Without this, Jupyter just shows a raw list of object memory locations.

Interactive HTML Representation (Jupyter)

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>\"\"\"
Enter fullscreen mode Exit fullscreen mode

_repr_html_() isn’t technically part of __str__/__repr__, but it's closely related. It allows Jupyter to display rich previews.

Expert Tip: Use a Repr Mixin

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')
Enter fullscreen mode Exit fullscreen mode

Conclusion

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.

  • Use __str__ for people
  • Use __repr__ for devs
  • Use __format__ for customization

And never underestimate their power again.


The original post is here.

Top comments (0)