Skip to content

Fix locals()/vars() cell-variable sync for module/class scope - #8467

Merged
youknowone merged 1 commit into
RustPython:mainfrom
sigmaith:fix/locals-cellvar-scope-leak
Aug 8, 2026
Merged

Fix locals()/vars() cell-variable sync for module/class scope#8467
youknowone merged 1 commit into
RustPython:mainfrom
sigmaith:fix/locals-cellvar-scope-leak

Conversation

@sigmaith

@sigmaith sigmaith commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Summary

  • CPython's _PyFrame_GetLocals only syncs fastlocals/cells into locals() for function scope; for module/class scope it just returns the namespace dict as-is, since ordinary names there are already stored via StoreName, not fastlocals.
  • sync_visible_locals_to_mapping did the fastlocals/cell sync for every scope, so the implicit __conditional_annotations__ cell (added for PEP 649/749 deferred annotations) broke two ways:

  1. At module scope its cell is always empty (real writes go through StoreName), so syncing overwrote the dict's real value with None, deleting it and causing NameError on the next annotated statement.

  2. At class scope the cell is the only place the value lives (StoreDeref-only, never in the dict), so syncing instead leaked __conditional_annotations__ into locals()/dir(), which CPython never does.

Fix

Skip CO_FAST_CELL/CO_FAST_FREE slots for non-optimized (module/class) scope in sync_visible_locals_to_mapping, matching CPython's behavior.

Details

  • Verified against CPython 3.14.6 (both a running interpreter and the C source at the v3.14.6 tag): module codegen writes __conditional_annotations__ with StoreName into the dict; class codegen writes it with StoreDeref into the cell only. _PyFrame_GetLocals never reads cells for either scope — it returns the namespace dict directly for non-optimized scope.
  • The skip only targets CO_FAST_CELL/CO_FAST_FREE, not CO_FAST_HIDDEN — a hidden slot (PEP 709 inlined comprehensions) can also be a cell, and the existing hidden-variable check just above already decides per-slot whether a live hidden value should still appear in locals(). Gating on scope alone would have overridden that.

Testing

  • Added extra_tests/snippets/syntax_annotations_locals.py, covering both the module-scope corruption and the class-scope leak. Confirmed it fails with the pre-fix NameError on unfixed code and passes after the fix.
  • cargo test -p rustpython-codegen (783 tests), cargo test -p rustpython-vm --lib (55 tests): all pass.
  • CPython test suite: test_annotationlib (117/117), test_builtin (138/138), test_class (37/37) all pass.
  • cargo clippy / cargo fmt --check: clean.

AI assistance

Investigated, implemented, and tested with Claude Code assistance, including direct comparison against a local CPython 3.14.6 source checkout and binary; all findings and the fix were manually verified before submission.

Assisted-by: Claude Code:claude-sonnet-5

Summary by CodeRabbit

  • Bug Fixes

    • Corrected locals() behavior in module and class scopes to match Python semantics.
    • Preserved annotated local values when using conditional annotations and exec.
    • Prevented internal annotation details from appearing in class locals() or dir() results.
  • Tests

    • Added regression coverage for module and class annotation behavior.

…tside function scope

CPython's _PyFrame_GetLocals only syncs fastlocals/cells into locals()
for function scope; module/class scope just returns the namespace dict
as-is. sync_visible_locals_to_mapping did this for every scope, so the
implicit __conditional_annotations__ cell (used for PEP 649/749
deferred annotations) broke two ways: at module scope its cell is
always empty, so syncing overwrote the dict's real value with None,
causing NameError on the next annotated statement; at class scope its
cell is the only real value, so syncing leaked it into locals()/dir(),
unlike CPython.

Skip cell/free slots outside function scope to match CPython, and add
a regression snippet.

Closes RustPython#8379

Assisted-by: Claude Code:claude-sonnet-5
@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

sync_visible_locals_to_mapping now excludes cell and free variables for non-optimized scopes. Regression tests cover conditional annotations, locals(), vars(), exec, and dir() at module and class scope.

Changes

Locals synchronization

Layer / File(s) Summary
Scope-aware locals synchronization and regression coverage
crates/vm/src/frame.rs, extra_tests/snippets/syntax_annotations_locals.py
Non-optimized class and module scopes no longer copy cell or free variables into locals(). Tests verify annotation values, exec, locals(), vars(), and dir() behavior. Optimized function scopes retain synchronization.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related issues

Possibly related PRs

Suggested reviewers: youknowone, shaharnaveh

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change to locals()/vars() cell-variable synchronization for module and class scope.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@sigmaith sigmaith changed the title Fix locals()/vars() corrupting/leaking __conditional_annotations__ outside function scope Fix locals()/vars() cell-variable sync for module/class scope Aug 8, 2026
@moreal moreal added the z-ca-2026 Tag to track Contribution Academy 2026 label Aug 8, 2026

@youknowone youknowone left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great, thank you!

@youknowone
youknowone merged commit cf68876 into RustPython:main Aug 8, 2026
27 checks passed
@sigmaith
sigmaith deleted the fix/locals-cellvar-scope-leak branch August 8, 2026 13:46
kyokuping pushed a commit to kyokuping/RustPython that referenced this pull request Aug 9, 2026
…tside function scope (RustPython#8467)

CPython's _PyFrame_GetLocals only syncs fastlocals/cells into locals()
for function scope; module/class scope just returns the namespace dict
as-is. sync_visible_locals_to_mapping did this for every scope, so the
implicit __conditional_annotations__ cell (used for PEP 649/749
deferred annotations) broke two ways: at module scope its cell is
always empty, so syncing overwrote the dict's real value with None,
causing NameError on the next annotated statement; at class scope its
cell is the only real value, so syncing leaked it into locals()/dir(),
unlike CPython.

Skip cell/free slots outside function scope to match CPython, and add
a regression snippet.

Closes RustPython#8379

Assisted-by: Claude Code:claude-sonnet-5
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

z-ca-2026 Tag to track Contribution Academy 2026

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

3 participants