We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Modern Syntax and Modernisation step 8 of 18
The f-string format specification mini-language
Half the %-formatting and .format() still in production exists because the
author did not know f-strings could pad, align, group and round. The other half
exists because nobody realised a class can own its own presentation rules.
The full replacement field
{ expression [=] [!s|!r|!a] [:format_spec] }
and the format spec itself is:
[[fill]align][sign][z][#][0][width][grouping][.precision][type]
The pieces you will actually reach for:
-
fill/align —
<left,>right,^centre,=sign-aware zero padding.f"{name:*^20}"centres in twenty columns padded with asterisks. -
width — a minimum, never a maximum.
f"{'overlong':3}"returns the whole string. If you need truncation, slice; the spec will not do it for you. -
grouping —
,or_.f"{1234567:,}"is1,234,567. -
.precision — with
f, digits after the point; withs, a maximum string length. -
type —
f,e,%,d,x,b,o.
Two things people miss. First, any part of the spec can itself be a nested replacement field, which is how you get runtime-computed column widths:
f"{text:{align}{width}}"
Second, __format__ receives the spec verbatim. Python does not parse it
for you. So a domain type can define its own vocabulary, and
f"{total:accounting}" replaces the format_money(total, style="accounting")
helper that was being called from forty places and drifting in three of them.
What you are building
A table renderer and a money type.
def render_row(cells: Sequence[Sequence[str]], widths: Sequence[int]) -> str
cells is a list of [text, align] pairs where align is "<", ">" or
"^"; widths is the matching column widths. Join the rendered cells with
"|". Use one f-string with nested width and alignment fields — no
str.ljust, no manual space arithmetic.
class Money:
def __format__(self, format_spec: str, /) -> str
Money holds a Decimal amount and a currency code. Four specs:
| spec |
example output for 1234.5 / -1234.5 / 0 |
|---|---|
"" (default) |
USD 1,234.50 / USD -1,234.50 / USD 0.00 |
"plain" |
1234.50 / -1234.50 / 0.00 |
"accounting" |
USD 1,234.50 / (USD 1,234.50) / USD 0.00 |
".Nf" |
delegate straight to the Decimal |
Anything else raises ValueError.
Finally:
def solve(cells, widths, amounts, spec) -> dict[str, object]
Build a Money for each decimal string in amounts with currency "USD",
format each with spec, and return
{"row": <rendered row>, "money": [...], "error": ""} — or, if the spec is
unsupported, {"row": ..., "money": [], "error": "ValueError"}.
The typing detail that bites
object.__format__ declares its parameter positional-only. Write
def __format__(self, format_spec: str) -> str: # no slash
and mypy --strict reports a Liskov violation against object, because a
caller holding an object may only pass that argument positionally. The same
applies to __eq__, __contains__, __getitem__ and most other dunders. The
/ is load-bearing, not stylistic.
Never let __format__ return something surprising for the empty spec.
f"{obj}" and str(obj) are different code paths: format(obj, "") calls
__format__, and object.__format__ only falls back to __str__ when the
spec is empty. Define one and you have quietly redefined how your object
appears in every log line and every f-string in the codebase.
Stuck?
Python reference solution
Sign in to attempt this problem and reveal the reference solution.