Skip to content

Respect global declarations and real assignment targets in shadowing and rebinding - #363

Merged
hakancelikdev merged 1 commit into
claude/integrationfrom
fix/symtable-scopes
Sep 25, 2026
Merged

hakancelikdev merged 1 commit into
claude/integrationfrom
fix/symtable-scopes

Conversation

@hakancelikdev

Copy link
Copy Markdown
Owner

Follow-up to #350 / #359, from the second review of claude/integration.

Problems (all removed a used import; the result raised NameError)

  1. global in an inner scope was ignored.
    import json
    def f():
        json = None
        def g():
            global json
            return json.dumps(1)   # module-level json
    The walk treated f's local json as the binding. The same happened with global in a class body, and with the use in a function nested inside the global one.
  2. Comprehension variables and lambda walrus targets counted as rebinding. n = sum(1 for sep in xs), len([0 for sep in items]) and f = lambda: (sep := "x") were all treated as reassigning sep, so a later bare use of sep no longer counted.
  3. A store inside a type comment crashed the whole run with AttributeError, because the parsed type comment has no enclosing statement.

Fix

  • _is_shadowed walks function and class bodies between the use and the import. It returns "not shadowed" at the first one that declares the name global. nonlocal keeps walking, which matches CPython's resolution. Class bodies still never provide a binding for the functions inside them.
  • _collect_declarations (replaces _collect_local_bindings) returns both the local and the global names of a body, and is cached per node.
  • _is_unconditional_assignment only accepts the statement's own targets, possibly unpacked through Tuple/List/Starred. Comprehension variables and walrus targets under a lambda are not targets.
  • _statement() returns None when there is no enclosing statement.

I considered moving the whole analysis to symtable. It would answer local/global/free per scope, but it can't tell which enclosing function binds a free name, or where in the flow a rebinding happens. Mapping its tables back to AST nodes is also version-dependent (PEP 709 inlining, and 3.13+ annotation scopes). The targeted rules above follow the same resolution order as CPython.

Tests

Six new scope/ cases, all failing before this change: global_in_inner_function, global_in_class_body, global_in_enclosing_function, comprehension_target_is_not_rebinding, genexp_and_lambda_walrus_are_not_rebinding, type_comment_store. Full suite passes on 3.9 / 3.12 / 3.14 (283 passed), and pre-commit passes.

🤖 Generated with Claude Code

https://claude.ai/code/session_019P9bvuGwAyVCNUdsAYPB1V


Generated by Claude Code

…and rebinding

Three cases removed imports that are used:

- A function or class body between the use and a shadowing function that
  declares the name global makes the use refer to the module binding.
  _is_shadowed now stops there instead of treating the outer local as the
  binding.
- _is_rebound treated any store in a top-level assignment as rebinding the
  name, including comprehension variables and walrus targets inside lambdas,
  which are local to their own scope. Only the statement's own (possibly
  unpacked) targets count now.
- A store parsed from a type comment has no enclosing statement; walking up
  to it crashed the run with AttributeError. It is no longer counted as a
  rebinding.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019P9bvuGwAyVCNUdsAYPB1V
@hakancelikdev
hakancelikdev merged commit 235490f into claude/integration Sep 25, 2026
40 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants