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 = 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.

Activity

  1. sigmaith commented on Jul 26, 2026

    @sigmaith
    Contributor

    take

  2. added a commit that references this issue on Aug 8, 2026
    cf68876
  3. added a commit that references this issue on Aug 9, 2026
    c29965a
  4. added a commit that references this issue on Sep 16, 2026
    7545bb1
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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