Skip to content

← Stdlib Mastery step 4 of 55

Easy Primitives

defaultdict: group rows, hand back a plain dict

Group (key, value) rows into key -> [values], preserving first-seen key order, and return a container that behaves like a dict to every caller.

Implement two functions:

def group_by_key(rows: list[tuple[str, int]]) -> dict[str, list[int]]: ...

def solve(
    rows: list[tuple[str, int]], probe: str
) -> tuple[list[tuple[str, list[int]]], str, str]: ...

solve exercises group_by_key and reports what it got back, as a 3-tuple:

  1. list(grouped.items())taken after the probe below, in first-seen key order.
  2. type(grouped).__name__ — must be "dict".
  3. "KeyError" if grouped[probe] raises, "returned" if it hands back a value.

The production consequence. defaultdict inserts on read. A defaultdict that escapes a function exports that behaviour to every caller: a membership test written if grouped[k]: grows the mapping forever, a serialiser sees the dict change size while iterating, and a caller who reasonably expects KeyError gets an empty list instead. The annotation -> dict[str, list[int]] will not warn you, because a defaultdict is a dict as far as the type system is concerned. The invariant “no insert-on-read” is not expressible in the signature, so it has to be enforced by the code: build with defaultdict, return dict(grouped).

Notice that the probe happens before the items snapshot. If you return the defaultdict itself, probing an absent key inserts an entry and the item list changes too — the same bug caught twice.

Your submission must pass mypy --strict. defaultdict(list) on its own is a var-annotated error (“Need type annotation”) — the most useful error in the module, because it forces you to state the shape of the grouping.

Returns a tuple, and the first element is a list of tuples. The harness compares container types exactly.