Repository navigation
Module-scope annotation after locals()/vars()/exec raises NameError: __conditional_annotations__ #8379
Description
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 definedScope / 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 RustPythonRoot 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.
Activity
sigmaith commentedon Jul 26, 2026
take
Fix locals()/vars() corrupting/leaking __conditional_annotations__ ou…
Fix locals()/vars() corrupting/leaking __conditional_annotations__ ou…
Fix locals()/vars() corrupting/leaking __conditional_annotations__ ou…