Crash report
What happened?
The compiler-generated __annotate__ function (PEP 649) begins with a guard if .format > VALUE_WITH_FAKE_GLOBALS: raise NotImplementedError. That comparison is emitted as a COMPARE_OP without the "convert result to bool" flag, immediately followed by POP_JUMP_IF_FALSE — which requires a boolean operand. When __annotate__ is called with a format whose > comparison returns a non-bool, the raw value reaches the jump:
python: Python/generated_cases.c.h: _PyEval_EvalFrameDefault:
Assertion `PyStackRef_BoolCheck(cond)' failed.
Minimal reproducer (aborts on a debug build):
class NonBool:
def __gt__(self, other):
return None # any non-bool result
def f(a: int): # any annotated function/class/module has __annotate__
...
f.__annotate__(NonBool())
It reproduces via a module __annotate__, a function __annotate__, and a class __annotate_func__ alike (the codegen is shared). Original find was wsgiref.types.__annotate__(obj) where obj.__gt__ returns a non-bool.
Root cause
Python/codegen.c, codegen_setup_annotations_scope():
// if .format > VALUE_WITH_FAKE_GLOBALS: raise NotImplementedError
...
ADDOP_I(c, loc, COMPARE_OP, (Py_GT << 5) | compare_masks[Py_GT]); // no bool-cast bit (0x10)
NEW_JUMP_TARGET_LABEL(c, body);
ADDOP_JUMP(c, loc, POP_JUMP_IF_FALSE, body);
The COMPARE_OP oparg omits bit 0x10, which the eval loop checks to convert the comparison result to a bool (if (oparg & 16) in _COMPARE_OP, Python/bytecodes.c). POP_JUMP_IF_FALSE asserts its operand is a bool. Normal comparisons emit COMPARE_OP without the bit and rely on the later TO_BOOL-folding optimizer pass to set it — see the comment in codegen_addcompare():
// ... The fifth-lowest bit indicates whether the result should be converted to
// bool and is set later):
ADDOP_I(c, loc, COMPARE_OP, (cmp << 5) | compare_masks[cmp]);
But this guard emits COMPARE_OP directly followed by POP_JUMP_IF_FALSE with no intervening TO_BOOL, so the optimizer has nothing to fold and the bit is never set.
Disassembling the same > comparison two ways makes the difference concrete:
if x > 2: -> COMPARE_OP 148 (bool(>)) # bit 0x10 SET
<generated __annotate__> -> COMPARE_OP 132 (>) # bit 0x10 CLEAR
With a normal integer format (annotationlib.Format.VALUE, ...), int > int returns a real bool, so the assertion holds and the bug is latent. A format whose rich comparison returns a non-bool sends the raw object to the jump.
Behavior by build
- Debug build:
assert(PyStackRef_BoolCheck(cond)) fails → abort.
- Release build: no assert, but
POP_JUMP_IF_FALSE treats the non-Py_False value as truthy → __annotate__ spuriously raises NotImplementedError; and it consumes the operand without a decref (the jump assumes an immortal bool), leaking a heap comparison result.
More generally, the compiler is supposed to guarantee that every POP_JUMP_IF_FALSE/POP_JUMP_IF_TRUE operand is a bool; this codegen path breaks that guarantee for arbitrary format arguments.
Proposed fix
Set the bool-conversion bit on the guard's COMPARE_OP, matching what a normal if a > b: ends up with:
- ADDOP_I(c, loc, COMPARE_OP, (Py_GT << 5) | compare_masks[Py_GT]);
+ ADDOP_I(c, loc, COMPARE_OP, (Py_GT << 5) | compare_masks[Py_GT] | 16);
(Py_GT << 5) | compare_masks[Py_GT] | 16 == 148, identical to the oparg the optimizer produces for if a > b:. (Emitting a TO_BOOL after the COMPARE_OP would also work; maintainers may prefer a named constant over the literal 16.)
Found by the fusil fuzzer (@vstinner's fuzzer, revived by @devdanzin);
investigated and drafted by Claude Code (Opus 4.8).
CPython versions tested on:
CPython main branch, 3.16, 3.15, 3.14
Operating systems tested on:
Linux
Output from running 'python -VV' on the command line:
Python 3.16.0a0 (heads/main:bcf98ddbc40, Jul 4 2026, 15:04:52) [Clang 21.1.8 (6ubuntu1)]
Linked PRs
Crash report
What happened?
The compiler-generated
__annotate__function (PEP 649) begins with a guardif .format > VALUE_WITH_FAKE_GLOBALS: raise NotImplementedError. That comparison is emitted as aCOMPARE_OPwithout the "convert result to bool" flag, immediately followed byPOP_JUMP_IF_FALSE— which requires a boolean operand. When__annotate__is called with aformatwhose>comparison returns a non-bool, the raw value reaches the jump:Minimal reproducer (aborts on a debug build):
It reproduces via a module
__annotate__, a function__annotate__, and a class__annotate_func__alike (the codegen is shared). Original find waswsgiref.types.__annotate__(obj)whereobj.__gt__returns a non-bool.Root cause
Python/codegen.c,codegen_setup_annotations_scope():The
COMPARE_OPoparg omits bit0x10, which the eval loop checks to convert the comparison result to a bool (if (oparg & 16)in_COMPARE_OP,Python/bytecodes.c).POP_JUMP_IF_FALSEasserts its operand is a bool. Normal comparisons emitCOMPARE_OPwithout the bit and rely on the laterTO_BOOL-folding optimizer pass to set it — see the comment incodegen_addcompare():But this guard emits
COMPARE_OPdirectly followed byPOP_JUMP_IF_FALSEwith no interveningTO_BOOL, so the optimizer has nothing to fold and the bit is never set.Disassembling the same
>comparison two ways makes the difference concrete:With a normal integer
format(annotationlib.Format.VALUE, ...),int > intreturns a real bool, so the assertion holds and the bug is latent. Aformatwhose rich comparison returns a non-bool sends the raw object to the jump.Behavior by build
assert(PyStackRef_BoolCheck(cond))fails → abort.POP_JUMP_IF_FALSEtreats the non-Py_Falsevalue as truthy →__annotate__spuriously raisesNotImplementedError; and it consumes the operand without a decref (the jump assumes an immortal bool), leaking a heap comparison result.More generally, the compiler is supposed to guarantee that every
POP_JUMP_IF_FALSE/POP_JUMP_IF_TRUEoperand is a bool; this codegen path breaks that guarantee for arbitraryformatarguments.Proposed fix
Set the bool-conversion bit on the guard's
COMPARE_OP, matching what a normalif a > b:ends up with:(Py_GT << 5) | compare_masks[Py_GT] | 16== 148, identical to the oparg the optimizer produces forif a > b:. (Emitting aTO_BOOLafter theCOMPARE_OPwould also work; maintainers may prefer a named constant over the literal16.)Found by the fusil fuzzer (@vstinner's fuzzer, revived by @devdanzin);
investigated and drafted by Claude Code (Opus 4.8).
CPython versions tested on:
CPython main branch, 3.16, 3.15, 3.14
Operating systems tested on:
Linux
Output from running 'python -VV' on the command line:
Python 3.16.0a0 (heads/main:bcf98ddbc40, Jul 4 2026, 15:04:52) [Clang 21.1.8 (6ubuntu1)]
Linked PRs