match
match is Python’s pattern-matching statement, with one Typhon-specific rule: on a closed subject — a sealed union, an enum, Result[T, E], T?, bool or a string-literal union — every value must be covered (or case _: must be present).
Syntax
Same as Python 3.10+:
match SUBJECT: case PATTERN [if GUARD]: suite case PATTERN [if GUARD]: suite ...Patterns
Literal pattern
match n: case 0: return "zero" case 1 | 2 | 3: return "small"Capture pattern
match x: case n: # captures any value as `n` return n + 1Class pattern (positional)
match s: case Circle(radius): return 3.14 * radius * radius case Rectangle(width, height): return width * heightClass pattern (keyword)
match u: case User(id=0, name=n): return f"admin {n}" case User(name=n): return nClass patterns on built-in types (v0.9.0)
case TypeName() as binding: matches any value whose runtime type is TypeName and binds it under the narrowed shape. Useful as a typed fall-through over a wide-union subject:
def label(x: object) -> str: match x: case str() as s: return f"str:{s}" case int() as n: return f"int:{n}" case float() as f: return f"float:{f}" case list() as xs: return f"list:{len(xs)}" case dict() as d: return f"dict:{len(d)}" case _: return "other"The exhaustiveness pass also recognises case None: + a class pattern on the wrapped type as covering a T? subject:
def show(s: str?) -> str: match s: case None: return "<missing>" case str() as x: return x # no missing_return since v0.9.0 — the two arms cover str?case str() as s: works in both tyc check and tyc run since v0.9.0; before v0.9.0 the VM only matched class patterns on user-defined classes.
Since v0.10.0 the exhaustiveness pass also certifies three more shapes, so they no longer false-positive tyc::missing_return:
def flag(b: bool) -> str: match b: case True: return "on" case False: return "off" # exhaustive — both bool inhabitants covered
type Dir = "n" | "s"def step(d: Dir) -> int: match d: case "n": return 1 case "s": return -1 # exhaustive — every string-literal variant covered
def add(p: tuple[int, int]) -> int: match p: case (x, y): return x + y # exhaustive — fixed-arity tuple, length is staticVariadic tuple[int, ...] is intentionally excluded. A bool or string-literal match that misses a value reports tyc::non_exhaustive_match naming it (as well as tyc::missing_return when the function falls off the end); wrong-arity tuple patterns still fire.
Or pattern
match status: case "active" | "pending": return True case _: return FalseSequence pattern
match xs: case []: return "empty" case [head, *rest]: return f"head={head}, rest={rest}" case [first, *middle, last]: return f"{first}…{last}"Sequence patterns with star-capture work in the VM since v0.8.0.
Mapping pattern
match cfg: case {"host": h, "port": p}: connect(h, p) case {"host": h, **rest}: connect(h, default_port=5432) log_extras(rest)Mapping patterns with rest-capture (**rest) work in the VM since v0.8.0.
Guard
match s: case Circle(r) if r > 100: return "huge circle" case Circle(_): return "circle"A guarded case does not satisfy exhaustiveness for the matched variant — you still need an unguarded fallback.
Wildcard
case _: return "anything"Exhaustiveness on sealed unions
type Shape = Circle | Rectangle | Triangle
def area(s: Shape) -> float: match s: case Circle(r): return 3.14 * r * r case Rectangle(w, h): return w * h case Triangle(b, h): return 0.5 * b * hNo wildcard needed — the three cases cover the whole sealed union. Add a fourth variant to Shape and this match becomes tyc::non_exhaustive_match.
type Shape = Circle | Rectangle | Triangle | Square # added Square
# every match s: ... in the codebase now fails:# error[tyc::non_exhaustive_match]: non-exhaustive `match` on sealed union `Shape`: missing variant(s) SquareUse case _: for a deliberate catch-all (opts out of exhaustiveness):
match s: case Circle(_): return "circle" case _: return "polygon" # catches every other variant — no exhaustiveness checkingExhaustiveness on enums (v0.13.0)
An enum’s members form a closed set, so a match covering every member is exhaustive with no wildcard:
enum Dir: NORTH / SOUTH / EAST / WEST
def delta(d: Dir) -> tuple[int, int]: match d: case Dir.NORTH: return (0, 1) case Dir.SOUTH: return (0, -1) case Dir.EAST: return (1, 0) case Dir.WEST: return (-1, 0) # exhaustive — all four members coveredDrop a member and the match fails, naming the one you missed:
# error[tyc::non_exhaustive_match]: non-exhaustive `match` on sealed union `Dir`: missing variant(s) WESTSince v0.14.1 enum exhaustiveness also carries across module boundaries — an enum imported from another module is checked the same way.
Nested sealed unions
A sealed union whose variants include another sealed union flattens to its leaf classes. Covering every leaf is exhaustive:
class Circle frozen: r: floatclass Rect frozen: w: float h: floatclass Tri frozen: b: float h: float
type Poly = Rect | Tritype Shape = Circle | Poly
def area(s: Shape) -> float: match s: case Circle(r): return 3.14 * r * r case Rect(w, h): return w * h case Tri(b, h): return 0.5 * b * h # exhaustive — every leaf coveredDrop the Tri arm and the match reports missing variant(s) Tri. The inner
alias is not a class, so case Poly(): cannot stand in for its variants: it
is rejected with
tyc::alias_not_a_class
(CPython raises TypeError on it). Write case Rect() | Tri(): instead.
Exhaustiveness on Result, T?, bool and literal unions
The same check covers every other closed subject, and reports the same
tyc::non_exhaustive_match:
Result[T, E]must handleOkandErr. WhenE(orT) is itself closed — a sealed union, anenum,boolor a literal union — the payload patterns must cover it too.T?must handleNoneonce the other arms coverT.boolmust handleTrueandFalse.- A string-literal union must handle every literal.
class NotFound frozen: path: strclass Timeout frozen: after_ms: int
type LoadError = NotFound | Timeout
def describe(r: Result[str, LoadError]) -> str: match r: case Ok(body): return body case Err(NotFound(path)): return f"missing: {path}" case Err(Timeout(after_ms)): return f"timed out after {after_ms}ms"
def label(x: int?) -> str: match x: case None: return "none" case int() as n: return str(n)Add Denied to LoadError and describe reports
missing variant(s) Err(Denied); drop case None: from label and it
reports missing variant(s) None. The [strictness] exhaustive-match
setting applies to all of these forms.
Matching over an expression scrutinee (v0.13.0)
The subject of a match doesn’t have to be a plain name — exhaustiveness runs the same analysis over any expression scrutinee:
match items[-1]: # last element of a sealed-union list case Circle(r): return 3.14 * r * r case Rectangle(w, h): return w * h case Triangle(b, h): return 0.5 * b * hMatch-arm scrutinee narrowing (v0.13.0)
A class pattern narrows the subject variable to the matched variant inside that arm, so attribute and method access on it type-check against the narrowed shape:
match action: case Action(_, _): # `action` is narrowed to `Action` here log(action.target)Severity
[strictness] exhaustive-match in typhon.toml:
"error"— default. Missing variants fail the build."warn"— flagged as a warning."off"— no exhaustiveness checks.
Keep it at "error" outside of refactor windows.
Pattern matching on Result
match parse("42"): case Ok(n): print(f"got {n}") case Err(msg): print(f"error: {msg}")Result[T, E] is a closed type, so the checker enforces both arms — and, when the error type is itself a sealed union, an Err(...) arm for each of its variants (see above).
How it desugars
Vanilla Python match:
match s: case Circle(r): return r * rmatch s: case Circle(r): return r * rTyphon’s check is at compile time; runtime semantics are unchanged.
Where next
- Sealed Unions — the type-system reference.
- Sealed Unions and Match (tour) — teaching page.
- Match Errors (diagnostics) —
non_exhaustive_matchreference.