Skip to content

B007: don't flag loop variables read by functions defined in the same scope - #600

Merged
cooperlees merged 3 commits into
PyCQA:mainfrom
DG-Elom:b007-closure-symtable
Oct 22, 2026
Merged

cooperlees merged 3 commits into
PyCQA:mainfrom
DG-Elom:b007-closure-symtable

Conversation

@DG-Elom

@DG-Elom DG-Elom commented Oct 14, 2026

Copy link
Copy Markdown
Contributor

Fixes #175

B007 reported a loop control variable as unused when its only readers were a function or lambda defined in the same scope as the loop:

def main():
    def f(x):
        print(i, x)

    for i in [1, 2, 3]:
        f("HI")

The variable now counts as used when a function defined in that scope reads it. Scopes are resolved with the standard library symtable, so shadowing (a parameter or local of the same name), nonlocal, global and class scopes follow CPython: free variables of the nested functions, or, for a loop at module level, the globals they read. symtable is only run when a loop has an unused control variable, once per file; if the source can't be compiled, no exemption is applied.

Tests are in tests/eval_files/b007.py, b007_module_scope.py and b007_py310.py (the issue example, functions defined before/after the loop, shadowing, global/nonlocal, classes, decorators, match). Running B007 over the CPython 3.13 stdlib and a site-packages tree (5,427 files), the only change is 7 fewer reports, each a loop variable read by a closure (e.g. Lib/test/test_range.py, Lib/test/pickletester.py); none are added. README changelog updated.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

Unresolved moderate issues affect scope correctness and analysis reliability.

1 open finding
What changed in this PR

Updates B007 to recognize loop-variable reads from nested functions and lambdas using symtable.

Changes:

  • Adds closure-aware and module-scope analysis.
  • Adds regression tests for scope, shadowing, and pattern matching.
  • Updates the changelog.

Unresolved issues remain around global and intermediate-scope bindings, nonlocal stores, repeated symbol-table scans, and source normalization.

File Description
tests/​eval_files/​b007.py Tests closure and shadowing scenarios.
tests/​eval_files/​b007_py310.py Tests pattern-matching cases.
tests/​eval_files/​b007_module_scope.py Tests module-level resolution.
README.rst Documents the B007 change.
bugbear.py Implements symbol-table-based closure detection.

🧠 Review effort: Lite


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread bugbear.py

@cooperlees cooperlees left a comment •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Let's fix the found globals effect please ...

… scope

A loop variable read as a free variable by a function or lambda defined in the same scope as the loop is used. Use the standard library symtable to resolve this like CPython does: free variables of the functions nested in the loop scope, or globals they read for a module-level loop. Fall back to no exemption when the source cannot be compiled. Fixes PyCQA#175.
@cooperlees
cooperlees force-pushed the b007-closure-symtable branch from c6338fa to 82d4177 Compare October 22, 2026 03:29

@cooperlees cooperlees left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Fixed the Copilot finding: when the enclosing scope declares the loop variable global, nested functions that read it have no free variable, so _closure_reads now also counts referenced globals when the name is global in the enclosing symtable table.

Details:

  • Direct reads (global i + def g(): return i) and deeply-nested reads are exempt.
  • Shadowing still flags B007: names bound locally in an intermediate function (free, not global in the reading table) or by a comprehension, and store-only global writes (is_referenced() == False).
  • nonlocal in the enclosing scope was already handled via free variables (verified with symtable).
  • Added 4 regression cases to tests/eval_files/b007.py.
  • Rebased onto current main (fixed README.rst changelog conflict; full suite: 128 passed, 2 skipped).

A 'global' declaration makes the loop target global too, in which case
nested readers have no free variable. Include referenced globals when
the enclosing symtable marks the name global.
@cooperlees
cooperlees force-pushed the b007-closure-symtable branch from 82d4177 to 633c973 Compare October 22, 2026 03:30

@cooperlees cooperlees left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Globals fix verified: enclosing-scope global reads (direct and deeply nested) are now exempt, while intermediate-local shadowing, comprehension shadowing, and store-only writes still flag. Rebased onto main, CI + pre-commit green.

@cooperlees
cooperlees merged commit bc1ec77 into PyCQA:main Oct 22, 2026
7 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.

Loop variable use in closure not considered

3 participants