String Representation¶
These dunder methods control how objects are converted to strings.
repr: Developer Representation¶
__repr__ provides an unambiguous, developer-friendly representation.
Basic repr¶
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¶
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¶
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) # <DatabaseConnection host='localhost' port=5432>
str: User-Friendly Representation¶
__str__ provides a readable, user-friendly string.
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¶
# 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¶
# 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')
format: Custom Formatting¶
__format__ enables custom format specifications.
Basic format¶
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¶
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¶
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.
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¶
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.
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¶
# 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¶
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¶
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,!ain f-strings to control conversion - Quote strings in repr with
!r:f"Cls({self.name!r})"