Python evaluates a comparison chain lazily: in a < b < c, if a < b is false, c is never evaluated. The assertion rewriter evaluates every comparator unconditionally, so a rewritten assert can call things Python would not call, and can fail with an unrelated exception instead of AssertionError.
def test_raises_the_wrong_error():
assert 1 < 0 < 1 / 0
def test_calls_what_it_should_not():
calls = []
def boom():
calls.append("boom")
return 5
try:
assert 1 < 0 < boom()
except AssertionError:
pass
assert calls == []
Both pass under --assert=plain and on unrewritten Python. Rewritten, the first raises ZeroDivisionError and the second finds calls == ["boom"].
The cause is in AssertionRewriter.visit_Compare: it walks the comparators in a loop, appending @py_assertN = <left> <op> <next> for each, and only then combines them with ast.BoolOp(ast.And(), ...). By the time the and runs, everything has already been evaluated.
This is the same family as #57 ("New assertion logic no longer follows Python short circuit logic"), which was fixed for and/or — visit_BoolOp nests each subsequent operand inside an ast.If on the previous result. Comparison chains never got the same treatment.
Tested on main (f306da747), Python 3.12.
Found while sweeping the rewriter for evaluation-order divergences alongside #14445.
Python evaluates a comparison chain lazily: in
a < b < c, ifa < bis false,cis never evaluated. The assertion rewriter evaluates every comparator unconditionally, so a rewrittenassertcan call things Python would not call, and can fail with an unrelated exception instead ofAssertionError.Both pass under
--assert=plainand on unrewritten Python. Rewritten, the first raisesZeroDivisionErrorand the second findscalls == ["boom"].The cause is in
AssertionRewriter.visit_Compare: it walks the comparators in a loop, appending@py_assertN = <left> <op> <next>for each, and only then combines them withast.BoolOp(ast.And(), ...). By the time theandruns, everything has already been evaluated.This is the same family as #57 ("New assertion logic no longer follows Python short circuit logic"), which was fixed for
and/or—visit_BoolOpnests each subsequent operand inside anast.Ifon the previous result. Comparison chains never got the same treatment.Tested on
main(f306da747), Python 3.12.Found while sweeping the rewriter for evaluation-order divergences alongside #14445.