Summary
At module scope, once a top-level annotation has run, calling locals() / vars() (or executing inside exec) between annotated statements erases the implicit __conditional_annotations__ cell binding, so the next annotated statement raises:
NameError: name '__conditional_annotations__' is not defined
CPython 3.14 handles all of these cases without error. The problem is specific to module scope; class scope is fine.
Reproduction
count: int = 1
_ = locals() # fast-to-locals sync drops the implicit cell binding
maybe: int = None # NameError: name '__conditional_annotations__' is not defined
Scope / trigger matrix (verified against CPython 3.14.5):
| case |
RustPython |
CPython 3.14 |
module: annotation → locals() → annotation |
❌ NameError |
✅ |
module: annotation → vars() → annotation |
❌ NameError |
✅ |
exec("a: int = 1\nlocals()\nb: int = 2") |
❌ NameError |
✅ |
module: if True: x: int = 1 → locals() → if True: y: int = 2 |
❌ NameError |
✅ |
module: two annotations, no locals() in between |
✅ |
✅ |
class scope: same locals()-between-annotations pattern |
✅ |
✅ |
module: "__conditional_annotations__" in dir() after an annotation |
False ❌ |
True |
Minimal failing script:
if True:
x: int = 1
_ = locals()
if True:
y: int = 2
print("ok") # never reached on RustPython
Root cause
RustPython creates __conditional_annotations__ as an implicit cell variable for scopes that need it, including module scope — crates/codegen/src/compile.rs:1896:
// Handle implicit __conditional_annotations__ cell if needed.
if Self::scope_needs_conditional_annotations_cell(ste) {
cellvar_cache.insert("__conditional_annotations__".to_string());
}
(plus the symbol-table plumbing in crates/codegen/src/symboltable.rs).
At module scope the binding is materialized into the namespace, but locals() runs the fast-to-locals sync (Frame::locals → sync_visible_locals_to_mapping in crates/vm/src/frame.rs:1324), and that sync does not preserve the __conditional_annotations__ cell binding. A subsequent annotation compiles to a LoadDeref/LoadClassDeref of that cell, which now finds it unbound and raises NameError.
Module scope is the affected case because a cell living in module scope is a rare Python-3.14 construct (introduced by conditional-annotation tracking); the class-scope path preserves it correctly.
Note the related divergence in the last table row: after a module-level annotation CPython exposes __conditional_annotations__ in the module namespace (dir() → True), while RustPython does not (False).
Reference
Verified against CPython 3.14.5. Surfaced by the syntax_annotations.py snippet.
Investigated and drafted by Claude; reviewed before filing.
Summary
At module scope, once a top-level annotation has run, calling
locals()/vars()(or executing insideexec) between annotated statements erases the implicit__conditional_annotations__cell binding, so the next annotated statement raises:CPython 3.14 handles all of these cases without error. The problem is specific to module scope; class scope is fine.
Reproduction
Scope / trigger matrix (verified against CPython 3.14.5):
locals()→ annotationNameErrorvars()→ annotationNameErrorexec("a: int = 1\nlocals()\nb: int = 2")NameErrorif True: x: int = 1→locals()→if True: y: int = 2NameErrorlocals()in betweenlocals()-between-annotations pattern"__conditional_annotations__" in dir()after an annotationFalse❌TrueMinimal failing script:
Root cause
RustPython creates
__conditional_annotations__as an implicit cell variable for scopes that need it, including module scope —crates/codegen/src/compile.rs:1896:(plus the symbol-table plumbing in
crates/codegen/src/symboltable.rs).At module scope the binding is materialized into the namespace, but
locals()runs the fast-to-locals sync (Frame::locals→sync_visible_locals_to_mappingincrates/vm/src/frame.rs:1324), and that sync does not preserve the__conditional_annotations__cell binding. A subsequent annotation compiles to aLoadDeref/LoadClassDerefof that cell, which now finds it unbound and raisesNameError.Module scope is the affected case because a cell living in module scope is a rare Python-3.14 construct (introduced by conditional-annotation tracking); the class-scope path preserves it correctly.
Note the related divergence in the last table row: after a module-level annotation CPython exposes
__conditional_annotations__in the module namespace (dir()→True), while RustPython does not (False).Reference
Verified against CPython 3.14.5. Surfaced by the
syntax_annotations.pysnippet.Investigated and drafted by Claude; reviewed before filing.