You need a function that takes an event decoded from JSON and returns the
path of a failed web request. Here it is twice, once with if and once with
match:
def failed_path_if(event: object) -> str | None:
if not isinstance(event, dict) or event.get("kind") != "http":
return None
status = event.get("status")
path = event.get("path")
if isinstance(status, int) and isinstance(path, str) and status >= 500:
return path
return None
def failed_path(event: object) -> str | None:
match event:
case {"kind": "http", "status": int() as status,
"path": str() as path} if status >= 500:
return path
case _:
return None
Both return the same result for any dictionary. The match version puts the
keys, their types, the names it binds and the guard in one place. The if
version spreads the same checks across two if statements and two lookups.
Use match when the branches are shapes of one value
match reads better when all three of these hold:
- Every branch looks at the same value, the subject.
- Each branch asks about its shape, such as its type, its length or its keys.
- The branch body uses pieces of that value.
Parsing commands, routing events and simplifying a tree of nodes, as in the exercises before this article, all fit.
Use if and elif for unrelated conditions
if user.is_admin:
allow()
elif quota_exceeded(user):
reject("quota")
elif now > deadline:
reject("late")
These conditions ask about different things, so there is no single subject.
With match, every branch would be case _ if ...:, which is an elif
chain with more words. Keep the if.
For a few plain values, such as a status that is "open", "closed" or
"pending", the two read about the same. Pick the one your team reads
faster.
When every branch calls a function with the same arguments, a dictionary of handlers can be shorter than either:
handlers = {"start": start_job, "stop": stop_job}
handler = handlers.get(command)
if handler is None:
raise ValueError(f"unknown command: {command}")
handler(job_id)
Common mistakes
Comparing with a bare name. A bare name in a case captures the subject,
so case PENDING: matches every value. To compare with a constant, write a
dotted name such as Status.PENDING, or use if status == PENDING:.
Trusting a dotted name you never tested. Python looks up Status.PENDING
only when it reaches that case. A typo such as Status.PENDNG raises
AttributeError at that moment, not when the module loads. Write a test
that reaches every case.
Treating a pattern as validation. A mapping pattern checks the keys you list and ignores the rest. That is fine for routing. Validation needs more, such as rejecting unknown keys, checking ranges and reporting clear errors. Write explicit checks for that, or use a validation library.
Check your understanding
A function checks user.is_admin, then user.is_banned, then len(cart) > 10. Should you rewrite it with match?
Show answer
Hide answer
No. The three conditions ask about different things, so there is no single
subject to match on. Every case would be case _ if ...:, which is an elif
chain with extra words. match is the better choice when the branches
describe shapes of one value and use pieces of it.
Check your understanding
Does match run faster than an if and elif chain?
Show answer
Hide answer
No. Python tries the cases in order, from the top, and stops at the first
case whose pattern and guard both succeed. That is the same kind of work an
elif chain does. Choose match for how the code reads. If speed matters
in a loop that runs often, time both versions with timeit.