Skip to content

B020: don't flag names bound in nested destructuring patterns (#521) - #581

Merged
cooperlees merged 2 commits into
PyCQA:mainfrom
aashish254:b020-nested-unpacking
Sep 28, 2026
Merged

cooperlees merged 2 commits into
PyCQA:mainfrom
aashish254:b020-nested-unpacking

Conversation

@aashish254

Copy link
Copy Markdown

B020 matched any name inside the loop target tree against the names loaded by the iterable expression. A name bound inside a nested destructuring pattern is not what the for statement rebinds on each iteration — the iterable expression reads the outer value exactly once, before iteration starts — so safe loops were reported:

for i, (ax, (series, def_name)) in enumerate(zip(axs, series)):
    ...  # B020 wrongly flagged `series`

(The two other shapes from #521: for smoother in smoother.smoothers is a genuine top-level shadow and stays flagged; the sibling-attribute cases were already fixed by #568.)

Fix

check_for_b020 now considers only the top-level bindings of the target (including Starred) as plain-name candidates. Whole dotted paths (for self.a in self.b) keep the path comparison from #568 unchanged, and top-level unpacking shadows like for key, values in values.items() are still reported.

Tests

  • New eval cases in tests/eval_files/b020.py: the issue-verbatim two-level unpacking, a one-level unpacking, a starred nested unpacking (all must stay clean), and a top-level tuple binding that must still fire.
  • Full suite pytest tests/ green (72 passed, 12 skipped — all skips are _PY version-gated eval files on this local 3.9 runner) including the self-clean flake8 checks; flake8 bugbear.py clean (the eval file intentionally contains flagged loops and is not linted); -k b902 pep8-naming env green; mypy bugbear.py shows no new errors on this env (8-error baseline is Python 3.9-specific — ast.TryStar, CI runs 3.13).
  • Regression-scoped witness: B020 output diffed before/after across all 71 eval files — zero flags changed except the nested-unpacking FPs this fix removes.
  • # OPTIONS-style hypothesis fuzz tests pass.

Fixes #521

Co-authored-by: Claude noreply@anthropic.com

A name bound inside a nested destructuring pattern is not what the for
statement rebinds each iteration; the iterable expression already read
the outer value once when it was evaluated. Matching the whole target
name tree reported safe loops like
`for i, (ax, (series, name)) in enumerate(zip(axs, series))`.

Co-authored-by: Claude <noreply@anthropic.com>
Keep B020 (PyCQA#521) entry alongside the 3.15/B913/B035 entries (PyCQA#580).

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

This is muse (muse-spark), posting on behalf of @cooperlees via his coding agent.

Reviewed the diff and verified locally: checked out the PR head and ran the full suite in an isolated venv — 84 passed (plus the 2 pre-existing skips on this 3.14 runner). Also merged current main to resolve the README UNRELEASED conflict (kept all entries) and pushed to b020-nested-unpacking; the merge result is green and the PR is MERGEABLE.

What I checked:

  • The #521 false positive is genuinely fixed: for i, (ax, (series, def_name)) in enumerate(zip(axs, series)) is clean on the PR branch, fires on main.
  • True positives preserved — I diffed old-vs-new B020 output on 11 target shapes: bare for a in a, top-level tuple shadow (for key, items in list(items)), top-level Starred, subscript targets, same-path attributes (for obj.value in obj.value), and the for smoother in smoother.smoothers case from #521 all still fire; sibling attributes (for self.a in self.b, #568) stay clean.
  • The nested-Starred edge (for (*a, b) in a) still fires via the top-level Starred unwrap — no behavior loss there. The only silenced shapes are genuinely nested bindings, which is exactly the intent.
  • NameFinder is still used elsewhere (B913UsageFinder subclasses it), so no dead import.
  • flake8 self-clean on bugbear.py: clean. mypy: same 4 pre-existing errors as main, no new ones. New eval cases in tests/eval_files/b020.py cover the issue-verbatim shape, one-level and starred nesting, plus a still-firing top-level case.

No changes requested. Approving for merge.

@cooperlees
cooperlees merged commit 47de1bb into PyCQA:main Sep 28, 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.

B020 false positives with iterable attribute acess

2 participants