Python Representation Practice Problems & Exercises
Practice: Representation and String Methods — __repr__, __str__, __format__ at Engineering Depth
← Back to lessonEasy
Predict the output of all three print statements. Pay attention to when Python uses __str__ versus __repr__.
class Coin:
def __init__(self, value, currency):
self.value = value
self.currency = currency
def __repr__(self):
return f"Coin({self.value!r}, {self.currency!r})"
def __str__(self):
return f"{self.value} {self.currency}"
c = Coin(1.5, "USD")
print(c)
print(repr(c))
coins = [Coin(0.25, "USD"), Coin(1, "EUR")]
print(coins)Solution
1.5 USD
Coin(1.5, 'USD')
[Coin(0.25, 'USD'), Coin(1, 'EUR')]
Explanation: print(c) converts via str(), which calls __str__ — producing the human-friendly "1.5 USD". repr(c) calls __repr__, producing the developer-friendly reconstructible form. The !r conversion flag in the f-string applies repr() to each field, adding quotes around strings. Crucially, when an object is a member of a container (list, dict, tuple), Python uses repr() on it — not str(). This is why the list shows quoted strings inside the Coin repr.
class Coin:
def __init__(self, value, currency):
self.value = value
self.currency = currency
def __repr__(self):
return f"Coin({self.value!r}, {self.currency!r})"
def __str__(self):
return f"{self.value} {self.currency}"
c = Coin(1.5, "USD")
print(c)
print(repr(c))
coins = [Coin(0.25, "USD"), Coin(1, "EUR")]
print(coins)Expected Output
1.5 USD\nCoin(1.5, 'USD')\n[Coin(0.25, 'USD'), Coin(1, 'EUR')]Hints
Hint 1: print(c) calls str(c) which calls __str__.
Hint 2: repr(c) calls __repr__ — notice the !r flag adds quotes around strings.
Hint 3: When objects appear inside a list, Python calls repr() on each element, not str().
Implement __repr__ so that eval(repr(obj)) recreates an equal object.
class RGB:
def __init__(self, r, g, b):
self.r = r
self.g = g
self.b = b
def __repr__(self):
return f"RGB({self.r}, {self.g}, {self.b})"
def __eq__(self, other):
if not isinstance(other, RGB):
return NotImplemented
return (self.r, self.g, self.b) == (other.r, other.g, other.b)
c = RGB(255, 128, 0)
print(repr(c))
c2 = eval(repr(c))
print(c == c2)Solution
def __repr__(self):
return f"RGB({self.r}, {self.g}, {self.b})"
Explanation: The contract for __repr__ is that it should, ideally, return a string that can be passed to eval() to recreate the object. The format ClassName(arg1, arg2, ...) mirrors the constructor call. For RGB(255, 128, 0), eval("RGB(255, 128, 0)") calls the constructor with those three integers, producing an equal object. When this round-trip is not possible (e.g., for objects with file handles), the convention is to use angle brackets: <ClassName attribute=value>.
class RGB:
def __init__(self, r, g, b):
self.r = r
self.g = g
self.b = b
def __repr__(self):
# TODO: return a string like RGB(255, 128, 0)
# that could be eval'd to recreate the object
pass
def __eq__(self, other):
if not isinstance(other, RGB):
return NotImplemented
return (self.r, self.g, self.b) == (other.r, other.g, other.b)
c = RGB(255, 128, 0)
print(repr(c))
c2 = eval(repr(c))
print(c == c2)Expected Output
RGB(255, 128, 0)\nTrueHints
Hint 1: The format should be ClassName(arg1, arg2, ...) so eval() can reconstruct it.
Hint 2: Use f"RGB({self.r}, {self.g}, {self.b})".
Hint 3: eval(repr(obj)) == obj is the gold standard for a good __repr__.
Predict all three outputs. There is no __str__ — observe how Python falls back to __repr__.
class Event:
def __init__(self, name):
self.name = name
def __repr__(self):
return f"Event(name={self.name!r})"
e = Event("launch")
print(e)
print(str(e))
print(repr(e))Solution
Event(name='launch')
Event(name='launch')
Event(name='launch')
Explanation: If a class defines only __repr__, Python uses it as the fallback for all string-conversion contexts: str(e), print(e), f-strings without the !r flag, and repr(e) all produce the same output. The reverse is not true — if you define only __str__, repr(e) still uses the default __repr__ (which produces something like <Event object at 0x...>). Therefore, if you define only one, make it __repr__.
class Event:
def __init__(self, name):
self.name = name
def __repr__(self):
return f"Event(name={self.name!r})"
# No __str__ defined
e = Event("launch")
print(e) # uses __repr__ as fallback
print(str(e)) # also uses __repr__
print(repr(e))Expected Output
Event(name='launch')\nEvent(name='launch')\nEvent(name='launch')Hints
Hint 1: When __str__ is not defined, Python falls back to __repr__ for all string-conversion contexts.
Hint 2: str(e) calls __str__; if absent, it calls __repr__.
Hint 3: All three outputs are identical in this case.
Predict all four f-string outputs, showing you understand the three conversion flags: !s, !r, and !a.
class Tag:
def __init__(self, content):
self.content = content
def __repr__(self):
return f"Tag({self.content!r})"
def __str__(self):
return f"<{self.content}>"
t = Tag("hello & world")
print(f"{t}")
print(f"{t!s}")
print(f"{t!r}")
print(f"{t!a}")Solution
<hello & world>
<hello & world>
Tag('hello & world')
Tag('hello & world')
Explanation: In f-strings, {obj} is equivalent to {obj!s} — both call str(obj). {obj!r} calls repr(obj). {obj!a} calls ascii(obj), which behaves like repr() but escapes any non-ASCII characters using \uXXXX sequences. For objects whose __repr__ produces only ASCII characters (as Tag does here), !r and !a produce identical output. The conversion flags are applied before any format spec, so {t!r:>30} first applies repr() then right-aligns in a 30-char field.
class Tag:
def __init__(self, content):
self.content = content
def __repr__(self):
return f"Tag({self.content!r})"
def __str__(self):
return f"<{self.content}>"
t = Tag("hello & world")
print(f"{t}")
print(f"{t!s}")
print(f"{t!r}")
print(f"{t!a}")Expected Output
<hello & world>\n<hello & world>\nTag('hello & world')\nTag('hello & world')Hints
Hint 1: {t} and {t!s} both call str(t) — they produce the same output.
Hint 2: {t!r} calls repr(t).
Hint 3: {t!a} calls ascii(t) — like repr() but escapes non-ASCII characters. For ASCII-only content it equals repr().
Medium
The Duration.__format__ is already implemented. Trace through the logic for 3665 seconds and predict all four outputs.
class Duration:
def __init__(self, seconds):
self.seconds = seconds
def __format__(self, spec):
if spec == "hms":
h = self.seconds // 3600
m = (self.seconds % 3600) // 60
s = self.seconds % 60
return f"{h}:{m:02d}:{s:02d}"
elif spec == "ms":
m = self.seconds // 60
s = self.seconds % 60
return f"{m}:{s:02d}"
else:
return f"{self.seconds}s"
def __repr__(self):
return f"Duration({self.seconds})"
d = Duration(3665)
print(format(d))
print(format(d, "hms"))
print(format(d, "ms"))
print(f"Elapsed: {d:hms}")Solution
3665s
1:01:05
61:05
Elapsed: 1:01:05
Explanation: 3665 seconds = 1 hour, 1 minute, 5 seconds. For "hms": 3665 // 3600 = 1 hour, (3665 % 3600) // 60 = 65 // 60 = 1 minute, 3665 % 60 = 5 seconds — giving 1:01:05. For "ms": 3665 // 60 = 61 minutes, 3665 % 60 = 5 seconds — giving 61:05. format(d) calls __format__ with an empty string spec, returning the default form. The f-string {d:hms} passes "hms" as the spec directly to __format__.
class Duration:
"""Time duration in seconds."""
def __init__(self, seconds):
self.seconds = seconds
def __format__(self, spec):
# spec can be: '' (default), 'hms' (H:MM:SS), 'ms' (M:SS)
if spec == "hms":
h = self.seconds // 3600
m = (self.seconds % 3600) // 60
s = self.seconds % 60
return f"{h}:{m:02d}:{s:02d}"
elif spec == "ms":
m = self.seconds // 60
s = self.seconds % 60
return f"{m}:{s:02d}"
else:
return f"{self.seconds}s"
def __repr__(self):
return f"Duration({self.seconds})"
d = Duration(3665)
print(format(d))
print(format(d, "hms"))
print(format(d, "ms"))
print(f"Elapsed: {d:hms}")Expected Output
3665s\n1:01:05\n61:05\nElapsed: 1:01:05Hints
Hint 1: __format__ receives whatever format spec appears after the colon in f"{obj:spec}".
Hint 2: For "hms": hours = seconds // 3600, minutes = (seconds % 3600) // 60, seconds = seconds % 60.
Hint 3: Use :02d to zero-pad minutes and seconds to two digits.
Implement __repr__ for Node so it produces a nested, reconstructible representation.
class Node:
def __init__(self, value, children=None):
self.value = value
self.children = children or []
def __repr__(self):
return f"Node({self.value!r}, {self.children!r})"
leaf1 = Node(2)
leaf2 = Node(3)
root = Node(1, [leaf1, leaf2])
print(repr(root))Solution
def __repr__(self):
return f"Node({self.value!r}, {self.children!r})"
Explanation: The !r flag calls repr() on self.children. When Python formats a list with repr(), it calls repr() on each element — so each Node in the list has its own __repr__ called, producing the nested output recursively. This works correctly to arbitrary depth. The result Node(1, [Node(2, []), Node(3, [])]) mirrors the constructor call exactly, so eval(repr(root)) would recreate the tree (given Node is in scope).
class Node:
def __init__(self, value, children=None):
self.value = value
self.children = children or []
def __repr__(self):
# TODO: produce Node(value, [child1, child2, ...])
# children should use their repr() recursively
pass
leaf1 = Node(2)
leaf2 = Node(3)
root = Node(1, [leaf1, leaf2])
print(repr(root))Expected Output
Node(1, [Node(2, []), Node(3, [])])Hints
Hint 1: Use !r on self.children to recursively call repr() on each child.
Hint 2: The format is Node(value, children_list).
Hint 3: f"Node({self.value!r}, {self.children!r})" works because lists call repr() on their elements.
Implement __format__ by delegating the format spec to the underlying amount field and appending the currency symbol.
class Money:
def __init__(self, amount, currency="USD"):
self.amount = amount
self.currency = currency
def __format__(self, spec):
formatted = format(self.amount, spec)
return f"{formatted} {self.currency}"
def __repr__(self):
return f"Money({self.amount!r}, {self.currency!r})"
m = Money(1234.567)
print(format(m, ".2f"))
print(format(m, ",.2f"))
print(f"Total: {m:,.2f}")Solution
def __format__(self, spec):
formatted = format(self.amount, spec)
return f"{formatted} {self.currency}"
Explanation: The cleanest approach is to delegate the format spec to the underlying numeric value using the built-in format() function, which applies the standard Python format mini-language (width, precision, fill, grouping separators, etc.) to self.amount. The result is then combined with the currency string. This means Money automatically supports every format spec that float supports — you do not need to parse the spec manually. This delegation pattern is idiomatic when your object wraps a value that already has rich formatting support.
class Money:
def __init__(self, amount, currency="USD"):
self.amount = amount
self.currency = currency
def __format__(self, spec):
# TODO: format self.amount using spec, then append currency symbol
# e.g. format(Money(1234.5), ".2f") -> "1234.50 USD"
# e.g. format(Money(1234.5), ",.2f") -> "1,234.50 USD"
pass
def __repr__(self):
return f"Money({self.amount!r}, {self.currency!r})"
m = Money(1234.567)
print(format(m, ".2f"))
print(format(m, ",.2f"))
print(f"Total: {m:,.2f}")Expected Output
1234.57 USD\n1,234.57 USD\nTotal: 1,234.57 USDHints
Hint 1: Delegate to the built-in float format: format(self.amount, spec).
Hint 2: Then append the currency: return f"{formatted} {self.currency}".
Hint 3: format(1234.567, ",.2f") produces "1,234.57" — the spec is the standard float format spec.
Study the use of reprlib.repr() to produce safe, truncated representations of objects with potentially large contents.
import reprlib
class LargeList:
def __init__(self, items):
self.items = list(items)
def __repr__(self):
return f"LargeList({reprlib.repr(self.items)})"
small = LargeList([1, 2, 3])
large = LargeList(range(1000))
print(repr(small))
print(repr(large))Solution
def __repr__(self):
return f"LargeList({reprlib.repr(self.items)})"
Explanation: The standard repr() on a list of 1000 integers would produce a multi-kilobyte string — inappropriate for logging or debugging contexts. reprlib.repr() is a drop-in replacement that truncates after a configurable limit (default: 6 items for lists, 30 characters for strings) and appends .... It also handles circular references gracefully by tracking already-seen objects. Using reprlib in your __repr__ is the production-grade approach for objects that may contain large or deeply-nested data.
import reprlib
class LargeList:
def __init__(self, items):
self.items = list(items)
def __repr__(self):
# TODO: use reprlib.repr() to produce a truncated representation
# of self.items so very long lists don't produce enormous repr strings
return f"LargeList({reprlib.repr(self.items)})"
small = LargeList([1, 2, 3])
large = LargeList(range(1000))
print(repr(small))
print(repr(large))Expected Output
LargeList([1, 2, 3])\nLargeList([0, 1, 2, 3, 4, 5, ...])Hints
Hint 1: reprlib.repr() produces a shortened representation of containers — it truncates after a configurable limit.
Hint 2: For a list of 1000 items, reprlib.repr shows the first few followed by "...".
Hint 3: The implementation is already provided — just read through it to understand the reprlib pattern.
Hard
Read the Direction.__format__ implementation carefully and predict all four outputs. Then explain how the fallback format(str(self), spec) enables standard width/fill/alignment specs.
class Direction:
_NAMES = {0: "N", 90: "E", 180: "S", 270: "W"}
def __init__(self, degrees):
self.degrees = degrees % 360
def __format__(self, spec):
if spec == "abbr":
return self._abbr()
elif spec == "deg":
return f"{self.degrees}deg"
else:
return format(str(self), spec)
def _abbr(self):
if self.degrees in self._NAMES:
return self._NAMES[self.degrees]
return f"{self.degrees}deg"
def __str__(self):
return f"{self.degrees} degrees"
def __repr__(self):
return f"Direction({self.degrees})"
d = Direction(270)
print(f"{d:abbr}")
print(f"{d:deg}")
print(f"{d:>20}")
print(f"{d}")Solution
W
270deg
270 degrees
270 degrees
Explanation: {d:abbr} passes "abbr" as the spec to __format__, which looks up 270 in _NAMES and returns "W". {d:deg} returns "270deg". {d:>20} is an unknown spec, so the fallback calls format(str(d), ">20") — this converts d to "270 degrees" (via __str__) and then right-aligns it in a 20-character field, producing 10 leading spaces. {d} with no spec calls __format__("") which also falls back to format(str(d), "") — equivalent to str(d). The fallback pattern format(str(self), spec) is the standard way to inherit Python's full alignment/width/fill support without re-implementing it.
class Direction:
"""Compass direction with custom formatting."""
_NAMES = {0: "N", 90: "E", 180: "S", 270: "W"}
def __init__(self, degrees):
self.degrees = degrees % 360
def __format__(self, spec):
# Support two custom specs, then fall back to standard str formatting:
# 'abbr' -> abbreviated name (N, E, S, W) or 'NNE' etc. for intermediates
# 'deg' -> '270deg'
# anything else -> pass to format(str(self), spec) for width/fill support
if spec == "abbr":
return self._abbr()
elif spec == "deg":
return f"{self.degrees}deg"
else:
return format(str(self), spec)
def _abbr(self):
if self.degrees in self._NAMES:
return self._NAMES[self.degrees]
return f"{self.degrees}deg"
def __str__(self):
return f"{self.degrees} degrees"
def __repr__(self):
return f"Direction({self.degrees})"
d = Direction(270)
print(f"{d:abbr}")
print(f"{d:deg}")
print(f"{d:>20}")
print(f"{d}")Expected Output
W\n270deg\n 270 degrees\n270 degreesHints
Hint 1: For spec "abbr", look up self.degrees in _NAMES or fall back to the degree string.
Hint 2: For spec "deg", return a simple formatted string.
Hint 3: For all other specs, convert self to str first then apply format(str_value, spec).
Trace the output of all four statements, demonstrating that type(self).__name__ produces the correct subclass name in inherited __repr__ implementations.
class AppError(Exception):
def __init__(self, message, code=None):
super().__init__(message)
self.code = code
def __str__(self):
if self.code is not None:
return f"[{self.code}] {self.args[0]}"
return self.args[0]
def __repr__(self):
if self.code is not None:
return f"{type(self).__name__}({self.args[0]!r}, code={self.code!r})"
return f"{type(self).__name__}({self.args[0]!r})"
class NotFoundError(AppError):
pass
class ValidationError(AppError):
def __init__(self, field, message):
super().__init__(message, code="VALIDATION")
self.field = field
def __str__(self):
return f"[{self.code}] {self.field}: {self.args[0]}"
def __repr__(self):
return f"ValidationError({self.field!r}, {self.args[0]!r})"
e1 = NotFoundError("User not found", code=404)
e2 = ValidationError("email", "invalid format")
print(str(e1))
print(repr(e1))
print(str(e2))
print(repr(e2))Solution
[404] User not found
NotFoundError('User not found', code=404)
[VALIDATION] email: invalid format
ValidationError('email', 'invalid format')
Explanation: NotFoundError inherits __str__ and __repr__ from AppError. In __repr__, type(self).__name__ evaluates to "NotFoundError" — not "AppError" — because type(self) is the actual class of the object, which is NotFoundError. This is the idiomatic way to write base-class __repr__ methods that automatically use the correct subclass name, avoiding the need to override __repr__ in every subclass. ValidationError overrides both methods for its specialised two-argument constructor form.
class AppError(Exception):
def __init__(self, message, code=None):
super().__init__(message)
self.code = code
def __str__(self):
if self.code is not None:
return f"[{self.code}] {self.args[0]}"
return self.args[0]
def __repr__(self):
if self.code is not None:
return f"{type(self).__name__}({self.args[0]!r}, code={self.code!r})"
return f"{type(self).__name__}({self.args[0]!r})"
class NotFoundError(AppError):
pass
class ValidationError(AppError):
def __init__(self, field, message):
super().__init__(message, code="VALIDATION")
self.field = field
def __str__(self):
return f"[{self.code}] {self.field}: {self.args[0]}"
def __repr__(self):
return f"ValidationError({self.field!r}, {self.args[0]!r})"
e1 = NotFoundError("User not found", code=404)
e2 = ValidationError("email", "invalid format")
print(str(e1))
print(repr(e1))
print(str(e2))
print(repr(e2))Expected Output
[404] User not found\nNotFoundError('User not found', code=404)\n[VALIDATION] email: invalid format\nValidationError('email', 'invalid format')Hints
Hint 1: str(e1) calls __str__ on NotFoundError, which inherits AppError.__str__. code=404 so it uses the [code] prefix.
Hint 2: repr(e1) calls __repr__: type(self).__name__ is "NotFoundError" (not "AppError").
Hint 3: ValidationError overrides both __str__ and __repr__ for its specialised format.
Read the Tree.__repr__ implementation and predict the exact output (including newlines and indentation). Then explain the recursive _repr design.
class Tree:
def __init__(self, value, children=None):
self.value = value
self.children = list(children or [])
def __repr__(self):
return self._repr(indent=0)
def _repr(self, indent):
prefix = " " * indent
if not self.children:
return f"{prefix}Tree({self.value!r})"
child_reprs = ",\n".join(c._repr(indent + 1) for c in self.children)
return f"{prefix}Tree({self.value!r}, [\n{child_reprs}\n{prefix}])"
t = Tree(1, [
Tree(2, [Tree(4), Tree(5)]),
Tree(3),
])
print(repr(t))Solution
Tree(1, [
Tree(2, [
Tree(4),
Tree(5)
]),
Tree(3)
])
Explanation: The public __repr__ delegates to _repr(indent=0). For leaf nodes, _repr returns a single-line string with the prefix. For internal nodes, it recursively calls _repr(indent + 1) on each child and joins them with ",\n". The prefix (two spaces per indent level) is prepended to each child's repr via the recursive call. This pattern — a public dunder that delegates to a private recursive helper carrying extra state — is the standard way to implement indented repr for tree structures. Python's own pprint module uses the same approach.
class Tree:
def __init__(self, value, children=None):
self.value = value
self.children = list(children or [])
def __repr__(self):
return self._repr(indent=0)
def _repr(self, indent):
prefix = " " * indent
if not self.children:
return f"{prefix}Tree({self.value!r})"
child_reprs = ",
".join(c._repr(indent + 1) for c in self.children)
return f"{prefix}Tree({self.value!r}, [
{child_reprs}
{prefix}])"
t = Tree(1, [
Tree(2, [Tree(4), Tree(5)]),
Tree(3),
])
print(repr(t))Expected Output
Tree(1, [\n Tree(2, [\n Tree(4),\n Tree(5)\n ]),\n Tree(3)\n])Hints
Hint 1: _repr is a helper that takes an indent level. Each level adds two spaces of padding.
Hint 2: Leaf nodes (no children) produce a single-line repr: Tree(value).
Hint 3: Internal nodes recursively call _repr(indent + 1) on each child and join with ",\n".
