Skip to content

B018: report generator expressions and lambdas used as statements - #604

Merged
cooperlees merged 3 commits into
PyCQA:mainfrom
LuisFigueroaG:fix/b018-generators-and-lambdas
Oct 22, 2026
Merged

cooperlees merged 3 commits into
PyCQA:mainfrom
LuisFigueroaG:fix/b018-generators-and-lambdas

Conversation

@LuisFigueroaG

@LuisFigueroaG LuisFigueroaG commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Refs #452.

B018 didn't report two kinds of statement that can never do anything:

(print(item) for item in items)  # lazy, print never runs
if not debug_func:
    lambda x: True  # meant debug_func = lambda x: True

Both were in the list of node types in #452. Like the operations from #579, a call in the part that runs right away (the first iterable of a generator expression, or a lambda's defaults) keeps it from being reported, and a generator expression or a lambda with defaults directly in a try/with block is left alone, since a test may build one to see that raise. f-strings stay out, as decided in #216.

On CPython 3.12's Lib (1,807 files) this adds 4 reports, all in interpreter tests that build these on purpose and already get B018 for bare constants. On Ubuntu's dist-packages (2,145 files) it adds 4: two are the bug above (UpdateManager/Core/utils.py and its copy in DistUpgrade), two are a gettext marker trick in apt/auth.py.

Tests are in tests/eval_files/b018_generators_lambdas.py. b023.py has one bare lambda that now also gets B018, so I added that expectation. tox py310–py315 and pre-commit pass.

A generator expression is lazy, so `(print(x) for x in items)` on its own
line never runs its body, and a lambda that is neither assigned nor passed
anywhere can never be called. Both are now reported like the other useless
expressions.

A generator expression directly in a `try` or `with` block is left alone,
like operations are, since it calls `iter()` on its first iterable right
away and a test may build one only to see that raise.

Refs PyCQA#452

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

Eager lambda defaults and generator iterable expressions can trigger false-positive diagnostics.

2 open issues
What changed in this PR

Extends B018 to detect discarded generator expressions and lambdas, continuing the useless-expression coverage from #452.

Changes:

  • Adds detection while exempting generators directly inside try/with blocks.
  • Adds diagnostic fixtures and updates an existing lambda expectation.
  • Documents the expanded coverage and changelog entry.
File Description
tests/​eval_files/​b023.py Adds B018 expectation for a bare lambda.
tests/​eval_files/​b018_generators_lambdas.py Covers discarded expressions, used values, and generator exemptions.
README.rst Documents expanded B018 coverage.
bugbear.py Adds generator-expression and lambda checks.

Review effort: Balanced

Comment thread bugbear.py Outdated
and node.value.func.id in FUNCTIONS_WITHOUT_SIDE_EFFECTS
)
# a lambda that is neither assigned nor passed anywhere is never called
or isinstance(node.value, ast.Lambda)

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.

Fixed in d97dad0: lambda defaults (positional and keyword-only) now go through the same side-effect check as operands, and a lambda with defaults directly in a try/with block is left alone. A lambda without defaults is still reported there, since it evaluates nothing. Both forms have regression cases.

Comment thread bugbear.py Outdated
and _is_useless_operation(node.value)
)
# a generator expression is lazy, so its body never runs
or isinstance(node.value, ast.GeneratorExp)

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.

Fixed in d97dad0: only generators[0].iter goes through the side-effect check, so (x for x in refresh()) and (x for x in await refresh()) are no longer reported, while calls in the body, filters or inner loops still are. Regression cases added.

…away

A generator expression evaluates its first iterable as soon as it's
created, and a lambda its defaults, so `(x for x in refresh())` still
calls `refresh()` and `lambda arg=record(): None` still calls `record()`.
Check those parts the same way operands of an operation are checked,
and leave a lambda with defaults directly in a `try` or `with` block
alone, since a default may be there only to see it raise.

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

Thanks again!

@cooperlees
cooperlees merged commit 25121e4 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.

3 participants