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 result
Both 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).