Skip to content

← Modern Syntax and Modernisation step 8 of 18

Easy Primitives

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:,}" is 1,234,567.
  • .precision — with f, digits after the point; with s, a maximum string length.
  • typef, 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.