skip to content

Which method does an f-string call for `{obj}`, `{obj!r}` and `{obj:>10}`?

level: middleimportance: should knowfreq 44%

answer

  1. A field is a format() call
  2. Conversions run before the spec
  3. Empty spec still reaches a dunder
  4. The inherited version rejects real specs
  5. !r bypasses a custom __format__

basics

~10 s

{obj} and {obj:>10} both call type(obj).__format__(obj, spec), with an empty spec in the first case. {obj!r} applies repr() first and formats that plain string, so a custom __format__ is bypassed.

solid answer

~40 s

A replacement field compiles to a `format()` call: `f"{obj:>10}"` is `format(obj, '>10')`, which dispatches to `type(obj).__format__`. With no spec you still reach `__format__`, with an empty string. The inherited `object.__format__` returns `str(self)` for an empty spec and raises `TypeError` for any non-empty one — which is why `f"{obj:>10}"` fails on a class that defines only `__str__`. A conversion runs first and short-circuits your class: `!r` calls `repr()`, `!s` calls `str()`, `!a` calls `ascii()`, and the spec is then applied to that string by `str.__format__`. The debugging form `f"{obj=}"` uses `repr()` by default. `%`-formatting is a separate path entirely: `%s` calls `str()`, `%r` calls `repr()`, and `__format__` is never consulted.

code

python · 18 lines
python
class Res:
    def __repr__(self):
        return "REPR"

    def __str__(self):
        return "STR"

    def __format__(self, spec):
        return f"FORMAT({spec!r})"


r = Res()
print(f"{r}")            # FORMAT('')
print(f"{r:>8}")         # FORMAT('>8')
print(f"{r!r}")          # REPR
print(f"{r!s}")          # STR
print(f"{r=}")           # r=REPR
print("%s %r" % (r, r))  # STR REPR

go deeper

for a junior

Know the two conversions you will actually type: !r gives the repr and !s gives the str inside an f-string, and a plain {obj} gives the readable form. Recognise TypeError about a format string as a formatting problem.

for a middle

Be able to trace the dispatch: field to format() to type(obj).__format__, with conversions applied first and the spec then applied to the resulting string. Explain why object.__format__ rejects a non-empty spec.

for a senior

Show judgment about implementing __format__ on domain types: keep it cheap and side-effect free, delegate unknown specs so standard alignment keeps working, and know that %-style formatting bypasses it entirely in mixed codebases.

for a principal

Decide whether custom format specs are worth their cost at all. They are invisible to readers, untested by default, and inconsistent across formatting styles, so most teams are better served by explicit helper methods than by clever spec mini-languages.

Three dunders can produce the text of an object, and an f-string picks between them by rules that are easy to state once and impossible to guess. ## The default path is `__format__`, not `__str__` A replacement field `{obj:spec}` compiles to the equivalent of `format(obj, 'spec')`, and `format()` dispatches to `type(obj).__format__(obj, 'spec')`. Crucially, this happens even when you write no spec at all: `f"{obj}"` calls `__format__` with the empty string. The reason people believe it calls `__str__` is that the inherited implementation, `object.__format__`, returns `str(self)` when the spec is empty — so for a class that has not overridden `__format__`, the observable result is the `__str__` output. The dispatch and the outcome are different things, and the difference becomes visible the moment a spec appears. ## Non-empty specs on the default implementation raise `object.__format__` accepts only the empty spec; anything else raises `TypeError: unsupported format string passed to Plain.__format__`. So `f"{obj:>10}"` on a class that defines only `__str__` is a runtime error, not a padded string. This surprises people who assume the spec is applied to whatever text the object produced. - If you want your objects to accept the standard string specs, the one-line implementation is to delegate: `def __format__(self, spec): return format(str(self), spec)`, which hands the spec to `str.__format__`. - Types that define their own specs — dates, numbers — parse the spec themselves. ## Conversions run before the spec and bypass your class 1. The `!r`, `!s` and `!a` conversions are applied to the value first, producing a plain `str` via `repr()`, `str()` and `ascii()` respectively. 2. The format spec is then applied to *that string*, through `str.__format__`. So `f"{obj!r:>20}"` right-aligns the repr text in twenty columns, and a `__format__` you wrote on the class never runs. That is a feature: `!r` is how you say "give me the debugging view regardless of what this type's formatting logic thinks", which is why it is the right thing to reach for inside a `__repr__` implementation and in log messages about unexpected values. ## The self-documenting form `f"{obj=}"` prints the source text of the expression, an equals sign, and the value rendered with `repr()` by default — `d=Duration(94)`. Add an explicit conversion or spec to change that: - `f"{obj=!s}"` uses `str()`, - and `f"{obj=:>8}"` switches back to the `__format__` path with a spec. Reaching for `repr()` by default is the right choice for a debugging aid, for the same reason containers do it. ## `str.format` and `%` are different doors - `"{0:_^7}".format(obj)` follows exactly the same rules as the f-string: conversion first, then `__format__`. - Old-style `%`-formatting does not: `"%s" % obj` calls `str()`, `"%r" % obj` calls `repr()`, and `__format__` is never involved. If a codebase mixes the two styles, a class whose `__format__` does real work will render differently depending on which formatting syntax a caller happened to use — a good argument for keeping `__format__` consistent with `__str__` for the empty spec. ## A practical implementation shape When a type deserves custom specs, handle your own codes, treat the empty spec as `str(self)`, and delegate anything else to the underlying string or value so alignment and width keep working. Keep `__format__` pure and cheap, like the other two: it is called from logging and from templates, often in loops. ## Summary table to memorise - `{obj}` and `{obj:spec}` reach `__format__`. - `{obj!s}` reaches `__str__`, `{obj!r}` reaches `__repr__`, `{obj!a}` reaches `ascii()`, and in all three the spec then applies to a plain string. - `{obj=}` uses `repr()` unless told otherwise. - `print(obj)` and `str(obj)` reach `__str__` directly, with no `__format__` involved at all. ## Why the design has three doors at all - `__str__` answers "how does this read", - `__repr__` answers "what is this exactly", - and `__format__` answers "how does this render under a caller-supplied spec". The third exists because some types own a genuine spec language: a date accepts a date-format spec, a number accepts fill, alignment, grouping and precision. Without a separate hook, those types would have to parse specs inside `__str__`, which has no parameter to receive one. That separation is also why `!r` is a useful escape hatch — it says "ignore this type's rendering opinions and show me the debugging view" — and why `!r` is the right habit in a log message about an unexpected value. Logging `got %s` for a string that happens to be empty prints nothing readable; logging the repr prints `''` and tells you exactly what arrived.

  • Why does `f"{obj:>10}"` raise TypeError on a class that defines only `__str__`?
    Because the field dispatches to the inherited `object.__format__`, which accepts only the empty spec and raises `TypeError: unsupported format string passed to ...__format__` for anything else. The fix is to implement `__format__`, and the minimal implementation delegates: `return format(str(self), spec)`, which lets `str.__format__` handle width, fill and alignment.
  • How would you implement `__format__` so custom and standard specs both work?
    Interpret the spec codes your type owns first, return `str(self)` for the empty spec, and delegate everything else — `return format(str(self), spec)` for a text-like type, or `format(self.value, spec)` to pass alignment and precision down to an underlying number. That keeps `{d:hms}` and `{d:>8}` both working.
  • Why is `!r` the right conversion to use inside a `__repr__` implementation?
    Because it renders each interpolated field with `repr()` regardless of what that field's type does for formatting, so strings come out quoted and nested objects show their own reprs. Without it, `Video(title=intro)` cannot be distinguished from a title that was not a string at all — the repr stops being unambiguous.

saying these in an interview costs you the question

  • Saying f"{obj}" calls __str__ directly
  • Expecting !r to still reach a custom __format__
  • Assuming object.__format__ accepts any spec
  • Thinking %s formatting goes through __format__
  • Believing the format spec is passed to __repr__

context