Skip to content

Module-scope annotation after locals()/vars()/exec raises NameError: __conditional_annotations__ #8379

Description

@youknowone

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 = 1locals()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::localssync_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.

Metadata

Metadata

Assignees

Labels

C-compatA discrepancy between RustPython and CPythonz-ca-2026Tag to track Contribution Academy 2026

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions