Skip to content

← Everyday Standard Library Tools step 17 of 55

Medium Primitives

Count and Total Rows per Key with groupby

Sales rows arrive in no particular order, and the same region can appear many times. Write summarise, which returns one (count, total) tuple for each distinct key.

summarise([("a", 2), ("b", 10), ("a", -2)])
# {"a": (2, 0), "b": (1, 10)}

What to write

def summarise(rows: list[tuple[str, int]]) -> dict[str, tuple[int, int]]: ...
def solve(rows: list[tuple[str, int]]) -> dict[str, tuple[int, int]]: ...
  • Return one entry per distinct key. The same key may appear in several separate runs, so combine them all.
  • Count every row, including repeated rows and rows whose value is 0.
  • Add up the values as signed integers.
  • Return real tuples for (count, total), not lists.
  • The order of the keys in the dict does not matter.
  • solve calls summarise(rows) and returns its result.

Use itertools.groupby. It groups only neighbouring rows, so sort a copy of the rows by key first, and leave the caller's list unchanged. Sort and group with the same key. A named function by_name(row: tuple[str, int]) -> str makes the shared key clear, though a lambda with the right types also works.

Use each group inside the loop, before groupby moves to the next one. Either turn the group into a list, or count and sum it in one pass. A group has no len, and once you have looped over it, it is empty.

Edge cases

  • Empty input returns {}.
  • An empty string is a valid key.

What is wrong with the starter

The starter already uses each group in time, but it has two bugs. It handles each run of a key on its own, so a later run overwrites an earlier one. And it always reports a total of 0. Fix both. The tests check only the result, but the point of the exercise is to use groupby, not to collect the rows in a dict.