Skip to content

B905, B910: don't flag zip() and defaultdict() the module binds to something else - #610

Open
LuisFigueroaG wants to merge 2 commits into
PyCQA:mainfrom
LuisFigueroaG:fix/b905-b910-shadowed-names
Open

LuisFigueroaG wants to merge 2 commits into
PyCQA:mainfrom
LuisFigueroaG:fix/b905-b910-shadowed-names

Conversation

@LuisFigueroaG

Copy link
Copy Markdown
Contributor

B905 reports any bare zip() without strict=, and B910 any bare defaultdict(int), without checking that the name is the builtin or collections.defaultdict. When the module replaces it, the suggestion can be wrong. From fontTools:

# fontTools/cu2qu/ufo.py
_zip = zip


def zip(*args):
    """Ensure each argument to zip has the same length. ..."""
    if len(set(len(a) for a in args)) != 1:
        raise UnequalZipLengthsError(*args)
    return list(_zip(*args))

...
segments_by_location = zip(*[_get_segments(g) for g in glyphs])  # B905, but this zip() has no strict= and already checks lengths

The same goes for from mylib import zip, or a module with its own defaultdict class.

This uses _module_binding_is_from from #609 (the latest module-level binding wins, a try/except ImportError fallback still counts as the source module) for zip with builtins and defaultdict with collections. Still reported:

  • names the module never binds, which is the usual case.
  • from collections import defaultdict, from builtins import zip and star imports from those modules, unless a later module-level def or assignment replaces them.
  • zip = builtins.zip and defaultdict = collections.defaultdict.

Unlike B018, B031 and B911, only module-level statements count here. zip is a common name for methods and class attributes, and with the whole-module rule a single NpzFile.zip = None would hide every B905 in the module. On the corpus below that rule removed 104 B905 reports, 89 of which really were the builtin zip() (numpy's _npyio_impl.py, mypy's types.py and sortedcontainers, whose from itertools import izip as zip only runs on Python 2). So B905/B910 pass unbound=True to _module_binding_is_from, and a method, class attribute or if-guarded import of that name leaves the builtin.

On about 51,000 files (site-packages from ten project virtualenvs, Ubuntu's Python 3.12 stdlib and the repos in my workspace), B905 reports drop by 15 (out of about 6,400) and B910 stays the same. Nothing new is reported. The removed ones are the fontTools cu2qu/ufo.py case above, which is installed in three places.

New cases are in tests/eval_files/b905_shadowed*.py and tests/eval_files/b910_shadowed*.py. Four of them fail on main (the alias and nested cases pass there and guard the regression). tox py310–py315, mypy and pep8_naming pass, and so does pre-commit.

…mething else

B905 reported every bare zip() call without strict=, and B910 every
bare defaultdict(int), even when the module defines its own zip() or
defaultdict, or imports one from another library. Those may not take
strict= at all, or may not be collections.defaultdict.

Both now look at the module-level bindings of the name, like B018,
B031 and B911 do: the last one decides, and a name the module never
binds is the builtin/collections one. Unlike those checks, a method,
class attribute or local variable called zip doesn't count, since zip
is common enough that the whole-module rule would hide many real
reports (numpy, mypy and sortedcontainers all have one).

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

A moderate active issue remains because annotated module-level aliases are not recognized as source bindings, which can suppress valid B905/B910 warnings.

1 open issue
What changed in this PR

This PR updates B905 and B910 to account for module-level rebinding before reporting bare zip() and defaultdict(int) calls, while adding regression fixtures and documentation.

Changes:

  • Adds module-level binding resolution for builtin zip and collections.defaultdict.
  • Preserves reporting for recognized aliases and names that are not rebound.
  • Adds shadowing, import, alias, nested-scope, and class-attribute test cases.
  • Documents the updated behavior in README.rst.
  • Moderate issue: Annotated assignments such as zip: object = builtins.zip are not recognized as source bindings, which can incorrectly suppress valid B905/B910 reports. The binding analysis should handle ast.AnnAssign nodes with values.
File Summary
tests/​eval_files/​b910_shadowed.py Tests module-level and class-based shadowing of defaultdict.
tests/​eval_files/​b910_shadowed_import.py Tests defaultdict imports from non-collections modules.
tests/​eval_files/​b910_shadowed_alias.py Tests aliases to collections.defaultdict.
tests/​eval_files/​b905_shadowed.py Tests module-level function shadowing of zip.
tests/​eval_files/​b905_shadowed_nested.py Tests nested scopes and class attributes that should not affect module-level resolution.
tests/​eval_files/​b905_shadowed_import.py Tests imported zip bindings and replacements.
tests/​eval_files/​b905_shadowed_alias.py Tests aliases to builtins.zip.
README.rst Documents the updated B905 and B910 behavior.
bugbear.py Implements module-level binding-aware checks for B905 and B910; annotated source assignments still require handling.

Review effort: Lite

Comment thread bugbear.py
Comment on lines +761 to +763
def _module_binding_is_from(
module: ast.Module, source: str, name: str, unbound: bool = False
) -> bool:

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch, fixed in be37743: _binds_from_source now treats an AnnAssign with a value like a plain assignment, and a bare zip: object still isn't a binding. The helper is shared, so this also applied to B018, B031 and B911 (the latter two since 26.10.27). Each has a *_shadowed_alias_annotated.py case now.

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