We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Modern Syntax and Modernisation step 12 of 18
Class patterns, __match_args__ and an expression simplifier
A class pattern is rewritten by the compiler into an isinstance check plus a
sequence of attribute reads. Which attributes, and in which order, comes from
__match_args__ — a class-level tuple of strings.
@dataclass
class Add:
left: Expr
right: Expr
Add.__match_args__ # ('left', 'right')
case Add(a, b): is therefore exactly case Add(left=a, right=b):. Three
consequences worth internalising:
-
__match_args__is public API. Reordering the fields of a dataclass — a change that looks like formatting, that no type checker objects to, and that every keyword call site survives — silently reverses the meaning of every positional pattern in every consumer. In aSubtract(left, right)that is a wrong answer, not a crash. -
A class with no
__match_args__accepts zero positional subpatterns.case SomeClass(x):raisesTypeError: SomeClass() accepts 0 positional sub-patterns. Keyword subpatterns still work. -
kw_only=Truemakes__match_args__empty, because there are no positional__init__parameters to name.KW_ONLYtruncates it at the sentinel. So a dataclass refactor toward keyword-only arguments can break pattern matching in a different module.
A non-tuple __match_args__ raises TypeError at match time, not at class
creation — worth knowing when you hand-write one.
What you are building
A tiny expression compiler over five frozen dataclasses:
type Expr = Lit | Var | Neg | Add | Mul
The classes are given. You write four functions:
-
build(tokens: list[str]) -> Expr— parse reverse-Polish tokens."+","*"and"~"(unary negate) are operators; a token that parses as anintis aLit; anything else is aVar. Pop the right operand before the left. -
simplify(expr: Expr) -> Expr— bottom-up algebraic simplification:x * 0 → 0(either side),x * 1 → x(either side),x + 0 → x(either side),Lit(a) + Lit(b) → Lit(a+b),Lit(a) * Lit(b) → Lit(a*b),-Lit(n) → Lit(-n),-(-x) → x. -
render(expr: Expr) -> str—Litas its digits,Varas its name,Negas-x,Addas(l + r),Mulas(l * r). -
evaluate(expr: Expr, env: dict[str, int]) -> int.
solve(tokens, env) returns
{"original": render(built), "simplified": render(simplified), "value": evaluate(simplified, env)}.
Use positional class patterns (case Add(left, right):) and nest them
(case (Lit(0), _) | (_, Lit(0)):). That is the point of the exercise; a
chain of isinstance calls produces the same answers and none of the reading
practice.
The typing payoff
Because Expr is a closed union of five dataclasses and every match covers
all five, mypy --strict accepts these functions with no final return
and no else — it can prove the match is exhaustive. Delete one case and you
get Missing return statement for free. That is the property the next item
turns into a hard guarantee with assert_never.
Stuck?
Python reference solution
Sign in to attempt this problem and reveal the reference solution.