From this Stackoverflow Q&A. While PEP-0622 has a section on exhaustiveness checks on how it should be done, mypy currently (as of 1.9.0) fully relies on the return statements in all the arms to function correctly in the following example (derived using OP's self-answer):
from dataclasses import dataclass
@dataclass
class Mult:
left: 'Expr'
right: 'Expr'
@dataclass
class Add:
left: 'Expr'
right: 'Expr'
@dataclass
class Const:
val: int
Expr = Const | Add | Mult
def my_eval(e : Expr) -> int:
match e:
case Const(val):
return val
case Add(left, right):
return my_eval(left) + my_eval(right)
case Mult(left, right):
return my_eval(left) * my_eval(right)
def main():
print(my_eval(Const(42)))
print(my_eval(Add(Const(42), Const(45))))
if __name__ == '__main__':
main()If the Multi branch is omitted (e.g. by commenting the lines out), mypy will flag this following error:
exhaust.py:19: error: Missing return statement [return]
Likewise, pyright will do something similar:
/tmp/exhaust.py
/tmp/exhaust.py:19:26 - error: Function with declared return type "int" must return value on all code paths
"None" is incompatible with "int" (reportReturnType)
1 error, 0 warnings, 0 informations
Which is rather curious because they all imply return being involved. What if the code was restructured to not follow that? Consider the following my_eval:
def my_eval(e : Expr) -> int:
result: int
match e:
case Const(val):
result = val
case Add(left, right):
result = my_eval(left) + my_eval(right)
case Mult(left, right):
result = my_eval(left) * my_eval(right)
return resultBoth mypy and pyright will not complain, and neither should they as all branches are covered. But what if Multi was commented out?
$ mypy exhaust.py
Success: no issues found in 1 source file
$ pyright exhaust.py
/tmp/exhaust.py
/tmp/exhaust.py:28:12 - error: "result" is possibly unbound (reportPossiblyUnboundVariable)
1 error, 0 warnings, 0 informations
Welp, mypy fails, and pyright at least has a fallback, but given that pyright also punted this to an unbound variable, what if result was provided with a default value (e.g. result: int = 0)?
0 errors, 0 warnings, 0 informations
So effectively, downstream users of Expr can have no warning about whether or not they properly handle all cases that might be provided by Expr.
In languages that have more strict definitions of types, they will flag missing arms as a failure unless there is a explicit default branch, the following is roughly equivalent to the failing example in Rust:
enum Expr {
Multi {
left: Box<Expr>,
right: Box<Expr>,
},
Add {
left: Box<Expr>,
right: Box<Expr>,
},
Const {
val: i64,
}
}
fn my_eval(e: Expr) -> i64 {
let result;
match e {
Expr::Const {val} => result = val,
Expr::Add {left, right} => result = my_eval(*left) + my_eval(*right),
Expr::Multi {left, right} => result = my_eval(*left) * my_eval(*right),
}
result
}
fn main() {
let c = my_eval(Expr::Const { val: 42 });
let a = my_eval(Expr::Add {
left: Box::new(Expr::Const { val: 42 }),
right: Box::new(Expr::Const { val: 45 }),
});
println!("{c}");
println!("{a}");
}Commenting out the Multi arm will result in the compilation error.
error[E0004]: non-exhaustive patterns: `Expr::Multi { .. }` not covered
--> exhaust.rs:17:11
|
17 | match e {
| ^ pattern `Expr::Multi { .. }` not covered
This demonstrates that in Rust it is possible to rely on the compiler to catch fresh unhandled match branches, but this use case is (currently) not possible under Python, using mypy-1.9.0 or pyright-1.1.359 or earlier. This issue may be fixed for Nevermind that! PEP 634 supposedly the replacement to that earlier PEP 622 and now there is no exhaustiveness requirements, as per the comment that closed that issue as duplicate of python/mypy#13597. (Maybe a fix will be provided via that other issue, eventually?)mypy if python/mypy#17141 is resolved (maybe only for these variants and not others, I personally don't have high hopes).