Skip to content

← Modern Syntax and Modernisation step 10 of 18

Easy Primitives

Self-documenting f-strings: f"{x=}"

print(f"{user_id=}") is the single highest-value f-string feature for day-to-day work, because it deletes the commonest debug-print bug there is: a value printed under the wrong label after a copy-paste.

>>> total = 19.99
>>> f"{total=}"
'total=19.99'
>>> f"{total  =  }"
'total  =  19.99'

Three rules worth memorising:

  1. Whitespace inside the braces is preserved verbatim in the emitted text. f"{a = }" gives a = 2.
  2. = uses repr() by default — but supplying a conversion or a format spec switches it. f"{total=:.1f}" gives total=20.0, formatted, not repr’d.
  3. The expression can be arbitrary: f"{sum(xs)/len(xs)=}" emits the whole source text sum(xs)/len(xs)= followed by the value.

Rule 2 is why = defaults to repr: when you are debugging, you want to see the quotes, the escapes, and the Decimal('1.10') wrapper. str is for users; repr is for you.

What you are building

Two formatters plus the function under test.

def debug_repr(**values: object) -> str      # "name=repr(value)", joined by ", "
def display(**values: object) -> str         # "name=str(value)",  joined by ", "
def solve(pairs: list[list[object]]) -> str  # debug_repr + "\n" + display

solve receives an ordered list of [name, value] pairs, rebuilds them into a dict[str, object], and splats that dict into each formatter. Both formatters keep **values, so the **kwargs half of the exercise is intact — what changes is where the order comes from.

The pair list is deliberate. The output order is part of the answer, and the key order of a JSON object is not something you may rely on: a dict preserves insertion order, but the insertion order of a mapping that arrived over a wire is whatever the encoder and the store in the middle decided. A list of pairs has exactly one order, and it is the one that was written down.

With [["n", 42], ["label", "it's"]] that is:

n=42, label="it's"
n=42, label=it's

The second line is what a naive f"{name}={value}" produces. Notice what it costs you: label=it's and label="it's" are indistinguishable from label= followed by the unquoted contents, and an empty string simply vanishes.

Where this belongs, and where it does not

f"{x=}" is a debugging tool. Two rules for shipping it:

  • Never put an = field in a user-facing string. The expression source is emitted literally, so f"Sorry, {self._internal_retry_budget=}" leaks your variable names into a UI.
  • Never put one in a logging call. logging.debug(f"{payload=}") formats eagerly even when DEBUG is off, and destroys the structured record.args your aggregator groups on. Pass the value as an argument instead.

The decorator version is worth stealing for real code:

def trace_call[**P, R](fn: Callable[P, R]) -> Callable[P, R]:
    @functools.wraps(fn)
    def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
        logger.debug("%s(%s)", fn.__name__, debug_repr(**kwargs))
        return fn(*args, **kwargs)
    return wrapper

— the label always matches the value, because there is only one place that writes the label.