Match and Case
match picks the first shape that fits. Route a tool dict with case. Keep if for simple yes/no tests.
match looks at one value and runs the first case that fits. Fit means the shape matches: the keys you named are there, and the extra names fill in.
This is Python 3.10+. Joeven runs a newer Python, so it works here. Older books will not show it. You can do everything in this lesson with if and .get. match is a clearer way when the value is a dict with a few known shapes.
Use match when you branch on the shape of a dict (a tool call). Use if when you have a simple yes/no test: empty text, under budget, error is None.
match does not replace your allowlist. A case that binds any name still has to be checked against allowed tools, or you put only known names in the specific cases and let _ reject the rest. Routing is not the same as permission. Both are required.
match looks at a value
Write match value: then one or more case lines. Only the first fit runs. The rest are skipped.
Specific cases first. A leftover case last. If you put the wide case first, search never runs.
First shape that fits_ means “anything else.” Put it last. If you put it first, every value matches _ and the rest of the cases never run.
status = 429
match status:
case 200:
print(class="tok-s">"ok")
case 400:
print(class="tok-s">"bad request")
case 429:
print(class="tok-s">"slow down")
case _:
print(class="tok-s">"other")For a single number, if / elif is also fine. match shines when the value is a dict. Status codes still make a nice small example of first-fit.
The case body is indented. You can have several lines. You can return from a function inside a case.
Several numbers in one case use |: case 401 | 403:. That is “unauthorized or forbidden.” Do not pile policy into one case if the observation should differ. 401 and 429 are different stories for an agent: one is credentials, one is slow down.
Route a tool dict
A model action is often {"tool": "...", "args": {...}}. Each case can name the keys and bind the pieces.
def handle(action):
match action:
case {class="tok-s">"tool": class="tok-s">"search", class="tok-s">"args": {class="tok-s">"q": q}}:
return class="tok-s">"search " + q
case {class="tok-s">"tool": class="tok-s">"finish", class="tok-s">"args": {class="tok-s">"text": text}}:
return class="tok-s">"done: " + text
case {class="tok-s">"tool": name, class="tok-s">"args": args}:
return class="tok-s">"unknown " + str(name)
case _:
return class="tok-s">"bad action"Read the first case as: “if this is a dict with tool equal to search, and args is a dict with q, call that text q.” q is a new name filled from the dict. You do not write action["args"]["q"] inside that case. The match already pulled it out.
| Case | What it fits |
|---|---|
{"tool": "search", "args": {"q": q}} | Search with a query string |
{"tool": "finish", "args": {"text": text}} | Stop with an answer |
{"tool": name, "args": args} | Some other tool name |
_ | Not a dict, or missing keys |
The third case still needs args to be there. A dict with only {"tool": "search"} falls through to _. Extra keys in the dict are usually allowed; missing required keys are not. If q is missing, the search case does not fit.
If q is not a string, the case may still fit. Match is about structure more than about types. Check isinstance if you need a string.
| Situation | if / .get | match / case |
|---|---|---|
Empty text, budget, error is None | Better | Overkill |
| A dict that is search, finish, or other | Works, gets tall | Clearer shapes |
| Not a dict at all | if not isinstance(...) | case _: |
Need a type check on q | isinstance after .get | Still isinstance inside the case |
Clear beats clever. A five-line if that a beginner can trace is better than a clever pattern nobody dares to edit.
First fit wins
Put the specific cases first. Put the wide cases later.
If you put {"tool": name, "args": args} first, it would catch search too, and the search case would never run. That bug looks like “search is always unknown.” The match is working. Your order is wrong.
The same rule exists in if / elif. match makes the shapes visible, so the order mistake is a bit easier to see.
Walkthrough: swap two cases
def broken(action):
match action:
case {class="tok-s">"tool": name, class="tok-s">"args": args}:
return class="tok-s">"wide " + str(name)
case {class="tok-s">"tool": class="tok-s">"search", class="tok-s">"args": {class="tok-s">"q": q}}:
return class="tok-s">"search " + q
case _:
return class="tok-s">"bad"
print(broken({class="tok-s">"tool": class="tok-s">"search", class="tok-s">"args": {class="tok-s">"q": class="tok-s">"rain"}}))That prints wide search, not search rain. The wide case already fit. The search case is dead code. Python will not warn you. Tests will, if you have one test per case.
Write one test per case, including _. Include {"tool": "search"} with no args. Include a string, a list, and a dict with the wrong keys. Those are the leftover shapes.
Keep if for simple tests
Do not replace every if with match.
- Empty text?
if not text.strip(): - Under budget?
if step < max_steps: - Missing key?
if error is None:
match is for “this object looks like A, or B, or something else.”
You can match a tuple too: match pair: case ("search", q): .... That is the same first-fit idea. Tool dicts are the usual agent shape, so this lesson spends its pages there. Status numbers are the simple warmup.
If two cases look the same except for the tool string, you may want a registry dict instead of ten cases. match is for shapes. A dict of functions is for names. Combine them: match to confirm you have tool and args, then TOOLS[name](**args). The leftover _ still returns bad action when the object is not a dict.
What goes wrong
- Putting
case _:first. - Putting the wide tool case above specific tool names.
- Expecting
matchto check types. - Using
matchfor a boolean. - Forgetting that missing nested keys fail the case, which is often what you want.
- Matching the raw model string instead of the parsed dict.
- Treating extra keys as a failure — extra keys usually still fit.
A dict with extra keys like "id" still matches {"tool": "search", "args": {"q": q}}. Missing q does not. That split is the point: required shape vs leftover fields. If you need to forbid extra keys, that is a separate schema check, not match.
Do not match on raw model text. Parse first with json.loads. Then match the object. Text matching belongs to strings and regex, with all their substring traps. case "search": on the whole reply will miss "Let me search...".
Run to execute this in your browser. Nothing is sent to a server.
{"tool": "search"} has no args. Predict bad action, then confirm. That miss is a parser problem, not a search problem. The extra k on search still fits the search case: extra keys are allowed.
How agents use this
After you parse JSON, you have a dict. match routes that dict to the right tool without a tall pile of ifs. Put exact tool names first. Put a catch-all _ last so a weird object becomes an error observation, not a crash. Guards like empty text still belong in if.
You can match on a status code after an HTTP-like result, or on a tool dict after json.loads. Do not match on raw model text. Parse first. Then match the object.
If your team does not know match, write if action.get("tool") == "search":. Same policy. The language feature is optional. The routing idea is not: every agent must map a shape to a function, and must have a leftover case that does not run unknown tools.
A good executor is two layers. Layer one: match (or if) confirms you have a dict with tool and args. Layer two: name in TOOLS and fn(*args). If you skip layer two, the wide case that binds name will call anything the model spelled. The leftover _ is for shape failures. The allowlist is for permission* failures. Return two different error strings so tests and traces can tell them apart.
When you add a tool, add a specific case only if that tool needs a unique shape (search needs q, finish needs text). Tools that share {name, args} belong in the registry, not in a growing pile of clones. match is the bouncer at the door. The dict of functions is the room inside.
Check your understanding