Skip to content

String Representation

These dunder methods control how objects are converted to strings.

Mental Model

__repr__ is for developers (unambiguous, ideally copy-pasteable to recreate the object), __str__ is for end users (clean, readable). When in doubt, implement __repr__ first -- Python falls back to it whenever __str__ is missing, so one good __repr__ covers both debugging and basic display.

repr: Developer Representation

__repr__ provides an unambiguous, developer-friendly representation.

Basic repr

```python class Point: def init(self, x, y): self.x = x self.y = y

def __repr__(self):
    return f"Point({self.x}, {self.y})"

p = Point(3, 4) print(repr(p)) # Point(3, 4) print(p) # Point(3, 4) (uses repr as fallback) print([p]) # [Point(3, 4)] ```

Guidelines for repr

```python class User: def init(self, name, email, role='user'): self.name = name self.email = email self.role = role

def __repr__(self):
    # Goal: Valid Python expression that recreates the object
    return f"User({self.name!r}, {self.email!r}, role={self.role!r})"

u = User("Alice", "alice@example.com", "admin") print(repr(u)) # User('Alice', 'alice@example.com', role='admin')

Can often recreate the object

u2 = eval(repr(u)) print(u2.name) # Alice ```

When Exact Repr Isn't Possible

```python class DatabaseConnection: def init(self, host, port): self.host = host self.port = port self._connection = self._connect() # Internal state

def _connect(self):
    return f"<connection to {self.host}:{self.port}>"

def __repr__(self):
    # Use angle brackets for non-reconstructible objects
    return f"<DatabaseConnection host={self.host!r} port={self.port}>"

conn = DatabaseConnection("localhost", 5432) print(conn) # ```

str: User-Friendly Representation

__str__ provides a readable, user-friendly string.

```python class Temperature: def init(self, celsius): self.celsius = celsius

def __repr__(self):
    return f"Temperature({self.celsius})"

def __str__(self):
    return f"{self.celsius}°C"

t = Temperature(25) print(repr(t)) # Temperature(25) print(str(t)) # 25°C print(t) # 25°C (print uses str) print([t]) # [Temperature(25)] (list uses repr) ```

str vs repr Priority

```python

print() and str() use str first, fall back to repr

repr() always uses repr

Containers (list, dict) always use repr for elements

class Example: def repr(self): return "repr"

def __str__(self):
    return "str"

e = Example() print(str(e)) # str print(repr(e)) # repr print(e) # str print([e]) # [repr] print(f"{e}") # str print(f"{e!r}") # repr ```

Always Implement repr

```python

If only one: implement repr

str will fall back to repr automatically

class MinimalClass: def init(self, value): self.value = value

def __repr__(self):
    return f"MinimalClass({self.value!r})"

m = MinimalClass("test") print(str(m)) # MinimalClass('test') print(repr(m)) # MinimalClass('test') ```

The One Rule

If you implement only one string method, make it __repr__. Python falls back to __repr__ when __str__ is not defined, so print() and str() will still produce useful output. The reverse is not true — if you only define __str__, the interactive console and containers will show the default unhelpful <MyClass object at 0x...> representation. Always start with __repr__; add __str__ only when you need a separate user-facing format.

format: Custom Formatting

__format__ enables custom format specifications.

Basic format

```python class Money: def init(self, amount, currency='USD'): self.amount = amount self.currency = currency

def __format__(self, spec):
    if spec == '':
        return f"{self.amount:.2f} {self.currency}"
    elif spec == 'short':
        return f"${self.amount:.2f}"
    elif spec == 'full':
        return f"{self.amount:.2f} {self.currency} (US Dollars)"
    else:
        # Pass spec to float formatting
        return f"{self.amount:{spec}} {self.currency}"

m = Money(1234.567) print(f"{m}") # 1234.57 USD print(f"{m:short}") # $1234.57 print(f"{m:full}") # 1234.57 USD (US Dollars) print(f"{m:,.2f}") # 1,234.57 USD print(format(m, 'short')) # $1234.57 ```

Complex Format Specifications

```python class Vector: def init(self, x, y, z): self.x = x self.y = y self.z = z

def __format__(self, spec):
    # Parse spec: [precision][format_type]
    # format_type: 'c' for cartesian, 'p' for polar-like

    if not spec:
        return f"({self.x}, {self.y}, {self.z})"

    # Extract precision and type
    precision = ''
    fmt_type = 'c'

    for char in spec:
        if char.isdigit() or char == '.':
            precision += char
        else:
            fmt_type = char

    if fmt_type == 'c':  # Cartesian
        if precision:
            return f"({self.x:{precision}f}, {self.y:{precision}f}, {self.z:{precision}f})"
        return f"({self.x}, {self.y}, {self.z})"
    elif fmt_type == 'n':  # Named
        if precision:
            return f"x={self.x:{precision}f}, y={self.y:{precision}f}, z={self.z:{precision}f}"
        return f"x={self.x}, y={self.y}, z={self.z}"
    else:
        raise ValueError(f"Unknown format type: {fmt_type}")

v = Vector(1.234, 5.678, 9.012) print(f"{v}") # (1.234, 5.678, 9.012) print(f"{v:.2c}") # (1.23, 5.68, 9.01) print(f"{v:.1n}") # x=1.2, y=5.7, z=9.0 ```

Datetime-Style Formatting

```python class Person: def init(self, first, last, birth_year): self.first = first self.last = last self.birth_year = birth_year

def __format__(self, spec):
    # %f = first, %l = last, %F = full, %y = year
    result = spec
    result = result.replace('%f', self.first)
    result = result.replace('%l', self.last)
    result = result.replace('%F', f"{self.first} {self.last}")
    result = result.replace('%y', str(self.birth_year))

    # If no spec, return full name
    if result == spec and not spec:
        return f"{self.first} {self.last}"

    return result

p = Person("Ada", "Lovelace", 1815) print(f"{p}") # Ada Lovelace print(f"{p:%l, %f}") # Lovelace, Ada print(f"{p:%F (%y)}") # Ada Lovelace (1815) print(f"{p:Dr. %l}") # Dr. Lovelace ```

bytes: Bytes Representation

__bytes__ returns a bytes representation of the object.

```python class Message: def init(self, text, encoding='utf-8'): self.text = text self.encoding = encoding

def __bytes__(self):
    return self.text.encode(self.encoding)

def __str__(self):
    return self.text

msg = Message("Hello, 世界!") print(str(msg)) # Hello, 世界! print(bytes(msg)) # b'Hello, \xe4\xb8\x96\xe7\x95\x8c!' ```

Binary Data Classes

```python class Pixel: def init(self, r, g, b, a=255): self.r = r self.g = g self.b = b self.a = a

def __bytes__(self):
    return bytes([self.r, self.g, self.b, self.a])

def __repr__(self):
    return f"Pixel({self.r}, {self.g}, {self.b}, {self.a})"

@classmethod
def from_bytes(cls, data):
    return cls(*data[:4])

red = Pixel(255, 0, 0) print(bytes(red)) # b'\xff\x00\x00\xff' print(list(bytes(red))) # [255, 0, 0, 255]

Round-trip

red2 = Pixel.from_bytes(bytes(red)) print(red2) # Pixel(255, 0, 0, 255) ```

bool: Truth Value

__bool__ defines when an object is considered truthy.

```python class Container: def init(self, items=None): self.items = items or []

def __bool__(self):
    return len(self.items) > 0

def __len__(self):
    return len(self.items)

empty = Container() full = Container([1, 2, 3])

print(bool(empty)) # False print(bool(full)) # True

if full: print("Container has items") # Prints this ```

bool vs len Fallback

```python

If bool not defined, Python uses len != 0

If neither defined, object is always truthy

class OnlyLen: def init(self, size): self.size = size

def __len__(self):
    return self.size

print(bool(OnlyLen(0))) # False (len is 0) print(bool(OnlyLen(5))) # True (len is not 0) ```

Practical Example: Complete Class

```python class Duration: """Represents a time duration."""

def __init__(self, seconds):
    self.total_seconds = int(seconds)

@property
def hours(self):
    return self.total_seconds // 3600

@property
def minutes(self):
    return (self.total_seconds % 3600) // 60

@property
def seconds(self):
    return self.total_seconds % 60

def __repr__(self):
    return f"Duration({self.total_seconds})"

def __str__(self):
    if self.hours:
        return f"{self.hours}h {self.minutes}m {self.seconds}s"
    elif self.minutes:
        return f"{self.minutes}m {self.seconds}s"
    else:
        return f"{self.seconds}s"

def __format__(self, spec):
    if spec == '':
        return str(self)
    elif spec == 'hms':
        return f"{self.hours:02d}:{self.minutes:02d}:{self.seconds:02d}"
    elif spec == 'ms':
        total_min = self.hours * 60 + self.minutes
        return f"{total_min:02d}:{self.seconds:02d}"
    elif spec == 's':
        return str(self.total_seconds)
    elif spec == 'verbose':
        parts = []
        if self.hours:
            parts.append(f"{self.hours} hour{'s' if self.hours != 1 else ''}")
        if self.minutes:
            parts.append(f"{self.minutes} minute{'s' if self.minutes != 1 else ''}")
        if self.seconds or not parts:
            parts.append(f"{self.seconds} second{'s' if self.seconds != 1 else ''}")
        return ', '.join(parts)
    else:
        raise ValueError(f"Unknown format spec: {spec}")

def __bool__(self):
    return self.total_seconds > 0

def __bytes__(self):
    # 4-byte big-endian representation
    return self.total_seconds.to_bytes(4, 'big')

Usage

d = Duration(3725) # 1h 2m 5s

print(repr(d)) # Duration(3725) print(str(d)) # 1h 2m 5s print(f"{d}") # 1h 2m 5s print(f"{d:hms}") # 01:02:05 print(f"{d:ms}") # 62:05 print(f"{d:s}") # 3725 print(f"{d:verbose}") # 1 hour, 2 minutes, 5 seconds print(bool(d)) # True print(bytes(d)) # b'\x00\x00\x0e\x8d'

Falsy duration

zero = Duration(0) print(bool(zero)) # False if not zero: print("No duration") # Prints this ```

f-string Conversion Flags

```python class Example: def repr(self): return "Example()"

def __str__(self):
    return "An Example"

e = Example()

Default (uses str)

print(f"{e}") # An Example

!r forces repr

print(f"{e!r}") # Example()

!s forces str (explicit)

print(f"{e!s}") # An Example

!a forces ascii() - escapes non-ASCII

class Unicode: def repr(self): return "Unicode('café')"

u = Unicode() print(f"{u!a}") # Unicode('caf\xe9') ```

Key Takeaways

  • Always implement __repr__ - it's the foundation
  • __repr__ should be unambiguous and developer-friendly
  • __str__ is for end-user display; falls back to __repr__
  • __format__ enables custom format specifications in f-strings
  • __bytes__ is for binary/serialization representations
  • __bool__ controls truthiness; falls back to __len__
  • Use !r, !s, !a in f-strings to control conversion
  • Quote strings in repr with !r: f"Cls({self.name!r})"

Exercises

Exercise 1. Create a Date class with year, month, day. Implement __repr__ to return Date(2024, 3, 15) and __str__ to return 2024-03-15. Also implement __format__ to support "{:long}" (returns "March 15, 2024") and "{:short}" (returns "03/15/24"). Default format uses __str__.

Solution to Exercise 1
class Date:
    MONTHS = ["", "January", "February", "March", "April", "May", "June",
               "July", "August", "September", "October", "November", "December"]

    def __init__(self, year, month, day):
        self.year = year
        self.month = month
        self.day = day

    def __repr__(self):
        return f"Date({self.year}, {self.month}, {self.day})"

    def __str__(self):
        return f"{self.year}-{self.month:02d}-{self.day:02d}"

    def __format__(self, spec):
        if spec == "long":
            return f"{self.MONTHS[self.month]} {self.day}, {self.year}"
        elif spec == "short":
            return f"{self.month:02d}/{self.day:02d}/{self.year % 100:02d}"
        return str(self)

d = Date(2024, 3, 15)
print(repr(d))      # Date(2024, 3, 15)
print(str(d))       # 2024-03-15
print(f"{d:long}")  # March 15, 2024
print(f"{d:short}") # 03/15/24

Exercise 2. Write a LogLevel class with a level attribute (string like "INFO", "WARNING", "ERROR"). Implement __repr__, __str__, and __bool__ (where "ERROR" is truthy for "has errors" checks). Show the difference between print(), repr(), and boolean context.

Solution to Exercise 2
class LogLevel:
    def __init__(self, level):
        self.level = level.upper()

    def __repr__(self):
        return f"LogLevel('{self.level}')"

    def __str__(self):
        return self.level

    def __bool__(self):
        return self.level == "ERROR"

info = LogLevel("INFO")
error = LogLevel("ERROR")

print(info)         # INFO
print(repr(error))  # LogLevel('ERROR')
if error:
    print("Has errors!")  # prints
if not info:
    print("No errors")   # prints

Exercise 3. Create a RichText class with text and style attributes. Implement __str__ (returns plain text), __repr__ (returns RichText('text', style='bold')), and __format__ (when spec is "html", returns "<b>text</b>" for bold style). Demonstrate all three outputs.

Solution to Exercise 3
class RichText:
    def __init__(self, text, style="plain"):
        self.text = text
        self.style = style

    def __str__(self):
        return self.text

    def __repr__(self):
        return f"RichText({self.text!r}, style={self.style!r})"

    def __format__(self, spec):
        if spec == "html":
            if self.style == "bold":
                return f"<b>{self.text}</b>"
            elif self.style == "italic":
                return f"<i>{self.text}</i>"
            return self.text
        return str(self)

rt = RichText("Hello", style="bold")
print(str(rt))       # Hello
print(repr(rt))      # RichText('Hello', style='bold')
print(f"{rt:html}")  # <b>Hello</b>