Repository navigation
B905, B910: don't flag zip() and defaultdict() the module binds to something else - #610
Open
LuisFigueroaG wants to merge 2 commits into
Open
LuisFigueroaG wants to merge 2 commits into
LuisFigueroaG wants to merge 2 commits into
Conversation
…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).
Contributor
There was a problem hiding this comment.
🟡 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
zipandcollections.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.zipare not recognized as source bindings, which can incorrectly suppress valid B905/B910 reports. The binding analysis should handleast.AnnAssignnodes 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 on lines
+761
to
+763
| def _module_binding_is_from( | ||
| module: ast.Module, source: str, name: str, unbound: bool = False | ||
| ) -> bool: |
Contributor
Author
There was a problem hiding this comment.
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

B905 reports any bare
zip()withoutstrict=, and B910 any baredefaultdict(int), without checking that the name is the builtin orcollections.defaultdict. When the module replaces it, the suggestion can be wrong. From fontTools:The same goes for
from mylib import zip, or a module with its owndefaultdictclass.This uses
_module_binding_is_fromfrom #609 (the latest module-level binding wins, atry/except ImportErrorfallback still counts as the source module) forzipwithbuiltinsanddefaultdictwithcollections. Still reported:from collections import defaultdict,from builtins import zipand star imports from those modules, unless a later module-leveldefor assignment replaces them.zip = builtins.zipanddefaultdict = collections.defaultdict.Unlike B018, B031 and B911, only module-level statements count here.
zipis a common name for methods and class attributes, and with the whole-module rule a singleNpzFile.zip = Nonewould hide every B905 in the module. On the corpus below that rule removed 104 B905 reports, 89 of which really were the builtinzip()(numpy's_npyio_impl.py, mypy'stypes.pyand sortedcontainers, whosefrom itertools import izip as ziponly runs on Python 2). So B905/B910 passunbound=Trueto_module_binding_is_from, and a method, class attribute orif-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.pycase above, which is installed in three places.New cases are in
tests/eval_files/b905_shadowed*.pyandtests/eval_files/b910_shadowed*.py. Four of them fail onmain(the alias and nested cases pass there and guard the regression). tox py310–py315, mypy and pep8_naming pass, and so does pre-commit.