Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 9

__repr__ vs. __str__: Developer vs. User Output

Recommended resources

Introduction

Welcome to the second module of our course, "Master Python's Data Model." In the previous module, we established the Pythonic mindset, culminating in a checklist to refactor common "Java-isms." You saw that the key to making your classes integrate smoothly with Python lies in its data model—the set of special "dunder" methods that define how objects behave.

Today, we take our first concrete step into implementing that data model. This lesson focuses on a fundamental aspect of any object: its string representation. Your learning outcome is to implement __repr__ for developer output and __str__ for user display, understanding the distinction.

You'll learn why a custom class's default printout is unhelpful, how to provide a clear, user-friendly summary with __str__, and how to create an unambiguous, debug-ready representation with __repr__.


The Default Representation: An Unhelpful Starting Point

When you define a class and create an instance of it, Python doesn't know how you want to see it as a string. Let's create a simple class and see what happens.

class StockHolding:
    def __init__(self, ticker, shares):
        self.ticker = ticker
        self.shares = shares




# Let's create an instance and try to print it
holding = StockHolding("AAPL", 100)

print(holding)

If you run this, you'll get something like:
<__main__.StockHolding object at 0x10f4b3c50>

This default output tells you the class name and its memory address, but it gives you no information about the actual object's state (ticker or shares). It's not useful for a user or for a developer trying to debug.

This short video clip introduces this exact problem and dismisses the common but unpythonic workaround of creating a custom .toString() method, a pattern you might be used to from Java.

String Conversion in Python: When to Use __repr__ vs __str__

Watch this segment to see the default, unhelpful string representation of a custom object and why you shouldn't invent your own 'toString' method to fix it.

Watch the video from the beginning until 01:57. Pay attention to the default output and the explanation of why custom methods like two_string() are not the Pythonic way.

Python provides a standardized, built-in way to solve this using two distinct dunder methods: __str__ and __repr__.


The Core Distinction: __str__ for Users, __repr__ for Developers

Python differentiates between two types of string representations for an object, each with a different audience and purpose.

  • __str__: Provides an informal, human-readable representation. It's what your end-users should see. Its goal is readability.
  • __repr__: Provides an official, unambiguous representation. It's for the developer. Its goal is to be informative and, ideally, allow you to recreate the object.
Visualizing __repr__ vs __str__ in Python
This image from Real Python perfectly summarizes the distinction: `__repr__` provides a detailed output for the programmer, while `__str__` offers a simplified output for the user.

Given your background in Java, you can think of __str__ as being very similar in purpose to Java's .toString() method. However, __repr__ introduces a new, developer-centric concept that is key to Python's design.

To get a solid grasp of this fundamental difference, please read the following brief article section.

In Short: Use .__repr__() for Programmers vs .__str__() for Users

This article, 'When Should You Use .repr() vs .str() in Python?', clearly explains the different target audiences for each method.

Read the introductory section titled 'In Short: Use .repr() for Programmers vs .str() for Users'. This will set the foundation for the rest of the lesson.


Implementing __str__: The User-Friendly Display

Let's start by providing a user-friendly representation for our StockHolding class. We do this by implementing the __str__ method. The print() function and the str() built-in will automatically call this method.

class StockHolding:
    def __init__(self, ticker, shares):
        self.ticker = ticker
        self.shares = shares

    def __str__(self):



        # Returns a user-friendly, readable string
        return f"A holding of {self.shares} shares in {self.ticker}"

holding = StockHolding("AAPL", 100)
print(holding)  # This now calls holding.__str__()

Output:
A holding of 100 shares in AAPL

This is much more informative for an end-user. The following video segment demonstrates this process clearly.

String Conversion in Python: When to Use __repr__ vs __str__

This clip shows how to add a __str__ method to a class and how it's automatically used by functions like print() to produce a clean, readable output.

Watch from 01:57 to 05:19. The video will walk you through adding __str__ and show how functions like print() and format() use it.

However, if you were to inspect this object in an interactive Python shell (REPL), you would still see the old, unhelpful default representation. That's because the REPL uses __repr__, not __str__.


Implementing __repr__: The Developer's Debug Tool

The primary goal of __repr__ is to be unambiguous. A good __repr__ should, if possible, be a valid Python expression that can recreate the object. This is incredibly useful for debugging.

Let's add a __repr__ to our class.

class StockHolding:
    def __init__(self, ticker, shares):
        self.ticker = ticker
        self.shares = shares

    def __str__(self):
        return f"A holding of {self.shares} shares in {self.ticker}"

    def __repr__(self):



        # Unambiguous representation, looks like a constructor call
        return f"StockHolding(ticker='{self.ticker}', shares={self.shares})"

holding = StockHolding("GOOG", 50)




# Used by print() and str()
print(str(holding))



# Output: A holding of 50 shares in GOOG




# Used by repr() and the interactive console
print(repr(holding))



# Output: StockHolding(ticker='GOOG', shares=50)




# In an interactive shell, just typing the variable name uses __repr__
# >>> holding
# StockHolding(ticker='GOOG', shares=50)

Notice the output of repr(holding). You could copy and paste that output StockHolding(ticker='GOOG', shares=50) directly into your code to recreate an identical object. This makes debugging much easier.

To see this principle applied perfectly in the standard library, let's look at the datetime object.

String Conversion in Python: When to Use __repr__ vs __str__

This segment uses the datetime object to perfectly illustrate the difference between a readable __str__ output and an unambiguous, recreatable __repr__ output.

Watch from 06:54 to 10:38. Focus on how str(today) produces a simple date string, while repr(today) gives you a full constructor call.


The Fallback Mechanism and a Golden Rule

So what happens if you only define one of the methods? Python has a simple fallback rule:

  1. If __str__ is not found, Python will use __repr__ in its place.
  2. However, __repr__ never falls back to __str__.

This leads to a simple but critical best practice: You should always implement __repr__ on your custom classes.

If you provide a __repr__, you guarantee a useful representation for both developers (in debuggers and REPLs) and for users (via print(), as a fallback). You should only add a __str__ if you need a different, more user-friendly representation than what your __repr__ provides.

This article provides a fantastic, in-depth walkthrough of this entire process, including the best practice of always defining __repr__.

When Should You Use .__repr__() vs .__str__() in Python?

This article walks through the process of defining both methods on a custom Book class. It clearly demonstrates the fallback behavior and reinforces the best practice.

Read the section 'Should You Define .repr() and .str() in a Custom Class?'. It covers the default behavior, the effect of adding each method, the fallback rule, and the recommended practice.

A final important detail is how container objects like lists and dictionaries handle string representation. When you print a container, it will always use the __repr__ of the items inside it, not their __str__. This is another reason why having a good __repr__ is essential for debugging.

holdings = [StockHolding("MSFT", 200), StockHolding("AMZN", 10)]
print(holdings)

Output:
[StockHolding(ticker='MSFT', shares=200), StockHolding(ticker='AMZN', shares=10)]

The list uses the __repr__ of each StockHolding object, giving you a detailed, debug-friendly view of its contents.


Exercise: Implement __str__ and __repr__

Now, it's your turn to apply these concepts. Below is a Transaction class. Your task is to implement both __str__ and __repr__ according to the principles we've discussed.




# transaction.py
from datetime import date

class Transaction:
    def __init__(self, tx_id: str, account_id: str, amount: float, tx_date: date):
        self.tx_id = tx_id
        self.account_id = account_id
        self.amount = amount
        self.tx_date = tx_date
    



    # --- YOUR CODE HERE ---
    # 1. Implement __repr__ to be unambiguous and recreatable.
    #    Example output: Transaction(tx_id='...', account_id='...', ...)




    # 2. Implement __str__ for a clean, user-facing summary.
    #    Example output: "Transaction [tx_id] on YYYY-MM-DD: $AMOUNT"




# --- Tests to verify your implementation ---
today = date(2023, 10, 27)
tx = Transaction(tx_id="tx_12345", account_id="acc_678", amount=99.95, tx_date=today)




# Test the __str__ output
expected_str = "Transaction [tx_12345] on 2023-10-27: $99.95"
assert str(tx) == expected_str




# Test the __repr__ output
# Note: The repr() of a date object is datetime.date(...)
expected_repr = "Transaction(tx_id='tx_12345', account_id='acc_678', amount=99.95, tx_date=datetime.date(2023, 10, 27))"



# We need to import datetime to eval the repr string
import datetime
recreated_tx = eval(repr(tx))

assert repr(tx) == expected_repr
assert recreated_tx.tx_id == tx.tx_id and recreated_tx.amount == tx.amount

Take your time to implement the two methods in the Transaction class. A correct implementation will make the assert statements pass.

Click to see a possible solution



# transaction.py (solution)
from datetime import date
import datetime # Needed for the repr

class Transaction:
    def __init__(self, tx_id: str, account_id: str, amount: float, tx_date: date):
        self.tx_id = tx_id
        self.account_id = account_id
        self.amount = amount
        self.tx_date = tx_date
    
    def __repr__(self):
        return (f"Transaction(tx_id='{self.tx_id}', "
                f"account_id='{self.account_id}', "
                f"amount={self.amount}, "
                f"tx_date={repr(self.tx_date)})")

    def __str__(self):



        # The :.2f formats the float to two decimal places
        return (f"Transaction [{self.tx_id}] on {self.tx_date.isoformat()}: "
                f"${self.amount:.2f}")


Conclusion

In this lesson, you've mastered the first and most common dunder methods for customizing your classes.

Key Takeaways:

  • __str__ is for users. It should be readable and provide a good summary. It is called by print() and str().
  • __repr__ is for developers. It should be unambiguous, detailed, and ideally, allow you to recreate the object. It's used by the REPL, debuggers, and inside containers.
  • The Golden Rule: Always implement __repr__. Only implement __str__ if you need a different, friendlier representation for users.
  • If __str__ is missing, Python falls back to __repr__.

You've taken a significant step in making your custom objects first-class citizens in Python. They no longer have a generic, unhelpful representation; they can now communicate their state clearly to both people and tools.

In our next lesson, we will continue our journey through the data model by exploring the iteration protocol. You'll learn how to implement __iter__ and __next__ so you can use your own objects in for loops, just like a built-in list or dictionary.