Skip to content

← Pattern Matching, f-Strings and Newer Syntax step 10 of 23

Medium Primitives

Match Lists and Dictionaries by Their Shape

A job queue takes commands from old and new clients. Some send a list such as ["move", 1, 2], some a dictionary such as {"type": "user", "id": 7}, and some a plain string. Describe each command by its shape.

solve([["move", 1, 2], "stop", {"type": "user", "id": 7, "extra": "x"}, 42])
# ["verb:move+2", "text:stop", "user:7", "scalar"]

Sequence patterns check the length and each item

match cmd:
    case []:                       # an empty list or tuple
        ...
    case [str() as verb]:          # exactly one item, and it is a string
        ...
    case [str() as verb, *rest]:   # a string, then any number of items
        ...

A sequence pattern such as [a, b, c] matches a list or a tuple of exactly three items. *rest collects the remaining items into a new list, and it can collect none. So [str() as verb, *rest] also matches ["stop"], with rest set to []. Put the one-item case first when it needs its own result.

Strings never match a sequence pattern

A string holds a sequence of characters, but sequence patterns skip strings on purpose. "abc" does not match [a, b, c], and "" does not match []. The same goes for bytes and bytearray. So the sequence cases can come first, and a later str() case still sees every string.

Mapping patterns ignore extra keys

A mapping pattern such as {"type": "user", "id": int() as uid} matches any dictionary that has those two keys with matching values. Other keys can be there too. So {} matches every dictionary, empty or not.

To reject extra keys, add **rest to the pattern and the guard if not rest. This exercise accepts extra keys.

Patterns nest. {"type": "batch", "items": [*members]} matches only when "items" holds a list or a tuple.

What to write

Fill in describe(cmd) with one match statement. Check the rows in this order. The first row that fits gives the result.

Command Result
Empty list or tuple "empty"
One item, a string v f"verb:{v}"
A string v, then n more items f"verb:{v}+{n}"
Any other list or tuple of length n f"seq:{n}"
A string s f"text:{s}"
bytes or bytearray "binary"
Dictionary with "type": "batch" and a list or tuple of length n under "items" f"batch:{n}"
Dictionary with "type": "user" and an integer u under "id" f"user:{u}"
Dictionary with a string k under "type" f"kind:{k}"
Any other dictionary "mapping"
Anything else "scalar"

Then fill in solve(commands). It returns the description of each command, in order, and leaves the input unchanged. An empty list gives [].

Edge cases:

  • int() also matches True, because bool is a subclass of int. So an id of True gives "user:True".
  • A string id fails the user row and falls through to "kind:user".
  • {"type": "batch", "items": "abc"} gives "kind:batch", because a string is not a sequence here. An empty items list gives "batch:0".
  • {"type": 3} gives "mapping", because its "type" value is not a string.
  • None, numbers and booleans give "scalar".