From 33202f5dd4e6db3f769c99f9c2fb848c8e4017f2 Mon Sep 17 00:00:00 2001 From: LuisFigueroaG Date: Thu, 22 Oct 2026 21:17:16 -0300 Subject: [PATCH] B018: report map(), filter() and other lazy builtins used as statements map(), filter(), reversed(), zip() and enumerate() return lazy iterators, so `map(print, items)` on its own line never calls `print`. They are now reported like the side-effect-free builtins B018 already covers. Like operations, a call directly in a `try` or `with` block is left alone, since these check their arguments right away and a test may call one only to see that raise. iter() is left out: it calls `__iter__` right away, which is what a bare `iter(x)` is for. The B905 and B912 tests call zip() and map() as statements, so their results are now assigned. Refs #452 --- README.rst | 14 ++++++--- bugbear.py | 16 ++++++++-- tests/eval_files/b018_lazy_calls.py | 47 +++++++++++++++++++++++++++++ tests/eval_files/b905_py310.py | 32 ++++++++++---------- tests/eval_files/b912_py314.py | 14 ++++----- 5 files changed, 94 insertions(+), 29 deletions(-) create mode 100644 tests/eval_files/b018_lazy_calls.py diff --git a/README.rst b/README.rst index 9a1b920..edcc363 100644 --- a/README.rst +++ b/README.rst @@ -178,10 +178,13 @@ The check also considers function calls without side-effects such as ``isinstanc unary and arithmetic operations such as ``-x`` or ``a * b``, generator expressions (which are lazy, so their body never runs) and lambdas (which nothing can call), as long as what they evaluate right away doesn't call a function, like -``(x for x in f())`` or ``lambda x=f(): x``. Shifts, bitwise operators and ``@`` are -left alone, since libraries overload them for their side effects, and so is an -operation, a generator expression or a lambda with defaults directly in a ``try`` or -``with`` block, where it may be run only to see it raise. +``(x for x in f())`` or ``lambda x=f(): x``. Calls to ``map``, ``filter``, +``reversed``, ``zip`` and ``enumerate`` are reported too: they are lazy, so in +``map(print, items)`` on its own line ``print`` is never called. Shifts, bitwise +operators and ``@`` are left alone, since libraries overload them for their side +effects, and so is an operation, a generator expression, a lambda with defaults or a +call to one of these lazy builtins directly in a ``try`` or ``with`` block, where it +may be run only to see it raise. Note that dangling commas will cause things to be interpreted as useless tuples. For example, in the statement ``print(".."),`` is the same as ``(print(".."),)`` which is an unassigned tuple. Simply remove the comma to clear the error. @@ -605,6 +608,9 @@ UNRELEASED * B018: also report generator expressions and lambdas used as statements. A statement like ``(print(x) for x in items)`` never runs its body, and a bare ``lambda x: True`` is usually an assignment that lost its target (#452) +* B018: also report ``map()``, ``filter()``, ``reversed()``, ``zip()`` and + ``enumerate()`` calls used as statements. They return lazy iterators, so + ``map(print, items)`` on its own line never calls ``print`` (#452) 26.9.30 ~~~~~~~ diff --git a/bugbear.py b/bugbear.py index df94b3b..218afe0 100644 --- a/bugbear.py +++ b/bugbear.py @@ -76,6 +76,10 @@ ast.Mod, ast.Pow, ) +# Builtins that return a lazy iterator, so `map(print, items)` as a statement +# never calls `print`. `iter` is left out: `iter(x)` calls `x.__iter__()` right +# away, which is what a bare `iter(x)` is usually for. +B018_LAZY_FUNCTIONS = ("enumerate", "filter", "map", "reversed", "zip") B018_RAISING_BLOCKS: tuple[type[ast.stmt], ...] = (ast.Try, ast.With, ast.AsyncWith) if sys.version_info >= (3, 11): B018_RAISING_BLOCKS += (ast.TryStar,) @@ -2501,12 +2505,20 @@ def check_for_b018(self, node: ast.AST) -> None: map(_is_useless_operation, _lambda_defaults(node.value)) ) ) + # `map()`, `filter()` and the like are lazy too, so the + # function or loop they were meant for never runs + or ( + isinstance(node.value, ast.Call) + and isinstance(node.value.func, ast.Name) + and node.value.func.id in B018_LAZY_FUNCTIONS + ) ) # directly in a `try` or `with` block, an operation is often run # only to see it raise: `with pytest.raises(TypeError): "1" + 1`. # The same goes for the first iterable of a generator expression, - # which `iter()` is called on, and the defaults of a lambda: - # `(x for x in 1)` or `lambda x=1 / 0: x` + # which `iter()` is called on, the defaults of a lambda, and the + # arguments of a lazy builtin, which it checks right away: + # `(x for x in 1)`, `lambda x=1 / 0: x` or `zip(1)` and not ( self.node_stack and isinstance(self.node_stack[-1], B018_RAISING_BLOCKS) diff --git a/tests/eval_files/b018_lazy_calls.py b/tests/eval_files/b018_lazy_calls.py new file mode 100644 index 0000000..131999d --- /dev/null +++ b/tests/eval_files/b018_lazy_calls.py @@ -0,0 +1,47 @@ +""" +Should emit: +B018 - on lines 11-18 and 21 +""" + + +def discarded(items, names, values, handle, fetch): + # map(), filter(), reversed(), zip() and enumerate() return lazy iterators: + # as a statement, nothing ever iterates over them, so the function they get + # is never called (#452) + map(print, items) # B018: 4, "Call" + map(lambda item: item.save(), items) # B018: 4, "Call" + filter(handle, items) # B018: 4, "Call" + reversed(items) # B018: 4, "Call" + zip(names, values, strict=True) # B018: 4, "Call" + enumerate(items) # B018: 4, "Call" + # `fetch` is still called here, but `handle` never is + map(handle, fetch()) # B018: 4, "Call" + for item in items: + if item: + map(handle, item) # B018: 12, "Call" + + +def used(items, names, values, handle, pool): + list(map(print, items)) + handled = map(handle, items) + for name, value in zip(names, values, strict=True): + handle(name, value) + pool.map(handle, items) + items.reverse() + # iter() calls `__iter__` right away + iter(items) + next(iter(items)) + return handled, filter(handle, items), [reversed(items), enumerate(items)] + + +def expected_to_raise(raises, obj): + # directly in a `try` or `with` block, a lazy builtin may be called only to + # see it raise, since it checks its arguments right away + with raises(TypeError): + zip(1, strict=True) + with raises(TypeError): + map(print, obj) + try: + reversed(obj) + except TypeError: + pass diff --git a/tests/eval_files/b905_py310.py b/tests/eval_files/b905_py310.py index ae4694e..eaa993f 100644 --- a/tests/eval_files/b905_py310.py +++ b/tests/eval_files/b905_py310.py @@ -1,22 +1,22 @@ -zip() # B905: 0 -zip(range(3)) # B905: 0 -zip("a", "b") # B905: 0 -zip("a", "b", *zip("c")) # B905: 0 # B905: 15 -zip(zip("a"), strict=False) # B905: 4 -zip(zip("a", strict=True)) # B905: 0 +_ = zip() # B905: 4 +_ = zip(range(3)) # B905: 4 +_ = zip("a", "b") # B905: 4 +_ = zip("a", "b", *zip("c")) # B905: 4 # B905: 19 +_ = zip(zip("a"), strict=False) # B905: 8 +_ = zip(zip("a", strict=True)) # B905: 4 -zip(range(3), strict=True) -zip("a", "b", strict=False) -zip("a", "b", "c", strict=True) +_ = zip(range(3), strict=True) +_ = zip("a", "b", strict=False) +_ = zip("a", "b", "c", strict=True) # infinite iterators from itertools module should not raise errors import itertools -zip([1, 2, 3], itertools.cycle("ABCDEF")) -zip([1, 2, 3], itertools.count()) -zip([1, 2, 3], itertools.repeat(1)) -zip([1, 2, 3], itertools.repeat(1, None)) -zip([1, 2, 3], itertools.repeat(1, times=None)) +_ = zip([1, 2, 3], itertools.cycle("ABCDEF")) +_ = zip([1, 2, 3], itertools.count()) +_ = zip([1, 2, 3], itertools.repeat(1)) +_ = zip([1, 2, 3], itertools.repeat(1, None)) +_ = zip([1, 2, 3], itertools.repeat(1, times=None)) -zip([1, 2, 3], itertools.repeat(1, 1)) # B905: 0 -zip([1, 2, 3], itertools.repeat(1, times=4)) # B905: 0 +_ = zip([1, 2, 3], itertools.repeat(1, 1)) # B905: 4 +_ = zip([1, 2, 3], itertools.repeat(1, times=4)) # B905: 4 diff --git a/tests/eval_files/b912_py314.py b/tests/eval_files/b912_py314.py index 6e8f423..7518a3b 100644 --- a/tests/eval_files/b912_py314.py +++ b/tests/eval_files/b912_py314.py @@ -1,8 +1,8 @@ -map(lambda x: x, "a", "b") # B912: 0 -map(lambda x: x, "a", "b", *map("c")) # B912: 0 -map(lambda x: x, "a", map(lambda x: x, "a"), strict=True) +_ = map(lambda x: x, "a", "b") # B912: 4 +_ = map(lambda x: x, "a", "b", *map("c")) # B912: 4 +_ = map(lambda x: x, "a", map(lambda x: x, "a"), strict=True) -map() -map(lambda x: x, "a") -map(lambda x: x, "a", "b", strict=False) -map(lambda x: x, "a", "b", "c", strict=True) +_ = map() +_ = map(lambda x: x, "a") +_ = map(lambda x: x, "a", "b", strict=False) +_ = map(lambda x: x, "a", "b", "c", strict=True)