Skip to content

← Modern Syntax and Modernisation step 18 of 18

Medium End-to-End

The walrus operator, tastefully

:= earns its keep in exactly three shapes. Outside them it is usually a way to make a line denser without making it clearer — and PEP 572’s own advice is blunt about that: “If either assignment statements or assignment expressions can be used, prefer statements.”

Shape 1 — capture the value you just tested

if (found := ENTRY_RE.match(line)) is not None:
    level = found["level"]

Without it you either call .match() twice, or you assign then test on two lines and lose the coupling. Under mypy --strict there is a third benefit: the walrus narrows Match[str] | None to Match[str] inside the branch, so you do not need an assert found is not None to silence the checker. That assert is one of the most common pieces of noise in strictly-typed code, and this deletes it.

Shape 2 — read until sentinel

while block := stream.read(size):
    process(block)

The pre-walrus form needs a priming read before the loop and a second read at the bottom — two call sites that must stay in sync, and the classic place for an off-by-one when someone changes the chunk size.

Shape 3 — comprehension filtering

[compute(x) for x in xs if compute(x) > 0]     # compute() runs TWICE per item
[y for x in xs if (y := compute(x)) > 0]       # once

This is not a micro-optimisation. When compute hits a database, charges an API, or is simply non-deterministic, double evaluation is a correctness bug — the value you filtered on is not necessarily the value you emitted.

The scoping rule behind it is deliberate and occasionally surprising: a walrus target inside a comprehension binds in the containing scope, not the comprehension’s own. That is what makes the value available in the element expression — and also what lets a comprehension quietly clobber a local called y.

What you are building

Four functions, then solve wiring them together.

  • parse_log(lines) -> list[tuple[str, str]] — for each line, strip it and match ENTRY_RE (given: an uppercase level, whitespace, a non-empty message). Non-matching lines are skipped. Return (level, message) pairs. A tuple, not a list — the grader compares container types.
  • chunks(stream: BinaryIO, size: int) -> Iterator[bytes] — yield successive reads until the stream is exhausted.
  • select_expensive(items, key, prefix) -> list[str]one comprehension returning the computed keys that start with prefix. key must run exactly once per item.
  • dedupe_expensive(items, key) -> list[T] — keep the first item per key, preserving order. key must run exactly once per item.

solve builds the BytesIO from blob, wraps the key function in a counter, and returns entries, chunks (as lengths), selected, deduped, select_calls and dedupe_calls. The call counts are asserted: a double evaluation is a test failure, not a style note.

The idiom that --strict rejects

The famous one-line dedupe is:

[x for x in xs if (k := key(x)) not in seen and not seen.add(k)]

mypy --strict rejects it: "add" of "set" does not return a value (it only ever returns None) [func-returns-value]. That check exists for a good reason and this is a genuine abuse of it. Which is why dedupe_expensive here is a statement loop with the walrus in the if — the readable form, the checkable form, and still one key call per item.

Loading visualization…