Skip to content

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

Medium End-to-End

Build an Event Router with Guards

A monitoring service receives events as dictionaries from web servers, timers and a sign-up form. Turn each one into a dataclass, then give it a label for the alert screen.

solve([
    {"kind": "http", "status": 503, "path": "/x"},
    {"kind": "timer", "seconds": 61},
    {"kind": "nope"},
])
# ["alert:/x", "slow-timer:61", "invalid"]

You will use mapping patterns to read the dictionaries and class patterns to route the dataclasses. The new part is how a guard changes which case runs.

A guard runs only after its pattern matches

match event:
    case Http(status=status, path=path) if status >= 500:
        return f"alert:{path}"
    case Http(path=path) if path.startswith("/admin"):
        return "forbidden"

Python checks the pattern first. It runs the guard only when the pattern matches, so the guard can use status and path. If the guard is false, Python moves on to the next case. So an Http event with status 200 fails the first guard and can still reach the /admin case. If the guard raises an exception, match does not catch it, and the exception goes up to the caller.

Combine | with as

(404 | 410) as status matches either number and stores the one that matched in status. It fits inside a class pattern:

match event:
    case Http(status=(404 | 410) as status, path=path):
        return f"gone:{status}:{path}"

A guard applies to the whole case. If the left side of a | matches and the guard is false, Python does not try the right side. It moves on to the next case.

What to write

parse(raw) uses one mapping pattern per kind, with int() and str() inside it to check the types:

Required entries Returns
"kind": "http", an integer "status", a string "path" Http(status, path)
"kind": "timer", an integer "seconds" Timer(seconds)
"kind": "user", a string "name", an integer "age" User(name, age)

Extra keys are fine. For a missing entry, a wrong type or an unknown kind, raise ValueError(f"unparseable event: {raw!r}"). parse checks types, not ranges, so a negative age still makes a User.

route(event) returns the label from the first row that fits:

Event Result
Http with status 500 or more f"alert:{path}"
Http with status 404 or 410 f"gone:{status}:{path}"
Http whose path starts with /admin "forbidden"
Any other Http f"ok:{path}"
Timer with more than 60 seconds f"slow-timer:{seconds}"
Any other Timer "timer"
User aged 18 or more f"adult:{name}"
Any other User f"minor:{name}"

The admin test is exactly path.startswith("/admin"), so /administrator matches and /Admin does not. The status rows come first, even on an admin path. On /admin, status 503 gives "alert:/admin", 404 gives "gone:404:/admin", and 200 gives "forbidden".

solve(events) parses and routes each dictionary, in order. When parse raises ValueError, add "invalid" for that event and go on to the next. An empty list gives []. Do not change the input dictionaries.