Skip to content

← Modern Syntax and Modernisation step 9 of 18

Medium Primitives

PEP 701: what f-strings can finally do in 3.12

Before 3.12 the f-string grammar was a separate, hand-written parser bolted onto the tokenizer. It could not see a backslash, could not reuse the quote character that opened it, and could not span lines. Every codebase of any age still carries the workarounds:

NL = "\n"                                   # module-level constant for a newline
label = d["key"]                            # temp var, only to dodge quote reuse
text = f"{label}{NL}"

PEP 701 rewrote f-strings into the PEG grammar in 3.12. Inside {...} you are now in ordinary Python:

  • Quote reuse. f"{d["key"]}" is legal. So is f"{f"{inner}"}", to any nesting depth.
  • Backslashes. f"{"\n".join(parts)}" is legal.
  • Multi-line expressions, in any f-string — not just triple-quoted ones — and # comments inside them.
  • Better errors. The tokenizer now points at the character that is wrong, not at the whole literal.

What did not change: an f-string still cannot be a docstring (it is not a constant), and lambda and := still need parentheses inside a replacement field, because a bare colon starts the format spec.

What you are building

def solve(rows: list[dict[str, object]], keys: list[str]) -> str

Render a fixed-column summary:

  1. a header line — the keys, joined with " | ";
  2. one line per row — for each key, the row’s value, joined with " | "; a key the row does not have renders as -;
  3. a footer line — "<n> row(s)".

The three parts are joined with newlines. With rows=[{"name": "ada", "age": 36}] and keys=["name", "age"]:

name | age
ada | 36
1 row(s)

Write it as one f-string — same-quote subscripting for the lookups, a "\n".join(...) inside a replacement field for the body, the expression broken across lines for readability. That form is the point of the exercise; the old workarounds produce identical output and teach nothing.

Note the distinction the tests care about: an absent key renders -, but a key that is present with the value None renders None. dict.get with a default cannot tell those apart on its own — reach for it deliberately.

The version wall

This is a syntax change. f"{d["key"]}" on 3.11 is a SyntaxError raised at import time, before any code in the module runs. That means it cannot sit behind if sys.version_info >= (3, 12):, cannot be feature-detected, and cannot be shipped to a 3.11 user with a graceful fallback. The only defence is requires-python = ">=3.12" in pyproject.toml, which is what makes the installer refuse the wheel instead of the interpreter refusing the import.