Skip to content

Conversation

@11happy
Copy link
Contributor

@11happy 11happy commented Nov 2, 2025

Summary

This PR re-implements return-in-generator (B901) for async generators as a semantic syntax error. This is not a syntax error for sync generators, so we'll need to preserve both the lint rule and the syntax error in this case.

It also updates B901 and the new implementation to catch cases where the generator's yield or yield from expression is part of another statement, as in:

def foo():
    return (yield)

These were previously not caught because we only looked for Stmt::Expr(Expr::Yield) in visit_stmt instead of visiting yield expressions directly. I think this modification is within the spirit of the rule and safe to try out since the rule is in preview.

Test Plan

I have written tests as directed in #17412

@github-actions
Copy link
Contributor

github-actions bot commented Nov 2, 2025

Diagnostic diff on typing conformance tests

No changes detected when running ty on typing conformance tests ✅

@github-actions
Copy link
Contributor

github-actions bot commented Nov 2, 2025

mypy_primer results

No ecosystem changes detected ✅

Memory usage changes were detected when running on open source projects
trio (https://github.com/python-trio/trio)
+ WARN expected `heap_size` to be provided by Salsa query `ClassLiteral < 'db >::variance_of_`
+ WARN expected `heap_size` to be provided by Salsa query `ClassLiteral < 'db >::variance_of_`
+ WARN expected `heap_size` to be provided by Salsa query `ClassLiteral < 'db >::variance_of_`
+ WARN expected `heap_size` to be provided by Salsa query `ClassLiteral < 'db >::variance_of_`
+ WARN expected `heap_size` to be provided by Salsa query `ClassLiteral < 'db >::variance_of_`
+ WARN expected `heap_size` to be provided by Salsa query `ClassLiteral < 'db >::variance_of_`
+ WARN expected `heap_size` to be provided by Salsa query `ClassLiteral < 'db >::variance_of_`
+ WARN expected `heap_size` to be provided by Salsa query `ClassLiteral < 'db >::variance_of_`
+ WARN expected `heap_size` to be provided by Salsa query `GenericAlias < 'db >::variance_of_`
+ WARN expected `heap_size` to be provided by Salsa query `GenericAlias < 'db >::variance_of_`
+ WARN expected `heap_size` to be provided by Salsa query `GenericAlias < 'db >::variance_of_`
+ WARN expected `heap_size` to be provided by Salsa query `GenericAlias < 'db >::variance_of_`
+ WARN expected `heap_size` to be provided by Salsa query `GenericAlias < 'db >::variance_of_`
+ WARN expected `heap_size` to be provided by Salsa query `GenericAlias < 'db >::variance_of_`
+ WARN expected `heap_size` to be provided by Salsa query `GenericAlias < 'db >::variance_of_`
+ WARN expected `heap_size` to be provided by Salsa query `GenericAlias < 'db >::variance_of_`
+ WARN expected `heap_size` to be provided by Salsa query `GenericAlias < 'db >::variance_of_`
+ WARN expected `heap_size` to be provided by Salsa query `GenericAlias < 'db >::variance_of_`
+ WARN expected `heap_size` to be provided by Salsa query `GenericAlias < 'db >::variance_of_`

sphinx (https://github.com/sphinx-doc/sphinx)
- WARN expected `heap_size` to be provided by Salsa query `GenericAlias < 'db >::variance_of_`
- WARN expected `heap_size` to be provided by Salsa query `GenericAlias < 'db >::variance_of_`
- WARN expected `heap_size` to be provided by Salsa query `ClassLiteral < 'db >::variance_of_`
- WARN expected `heap_size` to be provided by Salsa query `GenericAlias < 'db >::variance_of_`
- WARN expected `heap_size` to be provided by Salsa query `ClassLiteral < 'db >::variance_of_`
- WARN expected `heap_size` to be provided by Salsa query `GenericAlias < 'db >::variance_of_`
- WARN expected `heap_size` to be provided by Salsa query `GenericAlias < 'db >::variance_of_`

prefect (https://github.com/PrefectHQ/prefect)
- WARN expected `heap_size` to be provided by Salsa query `GenericAlias < 'db >::variance_of_`
- WARN expected `heap_size` to be provided by Salsa query `GenericAlias < 'db >::variance_of_`
- WARN expected `heap_size` to be provided by Salsa query `ClassLiteral < 'db >::variance_of_`
- WARN expected `heap_size` to be provided by Salsa query `ClassLiteral < 'db >::variance_of_`
- WARN expected `heap_size` to be provided by Salsa query `ClassLiteral < 'db >::variance_of_`
- WARN expected `heap_size` to be provided by Salsa query `ClassLiteral < 'db >::variance_of_`
- WARN expected `heap_size` to be provided by Salsa query `ClassLiteral < 'db >::variance_of_`
- WARN expected `heap_size` to be provided by Salsa query `ClassLiteral < 'db >::variance_of_`
- WARN expected `heap_size` to be provided by Salsa query `ClassLiteral < 'db >::variance_of_`
- WARN expected `heap_size` to be provided by Salsa query `GenericAlias < 'db >::variance_of_`
- WARN expected `heap_size` to be provided by Salsa query `GenericAlias < 'db >::variance_of_`
- WARN expected `heap_size` to be provided by Salsa query `GenericAlias < 'db >::variance_of_`
- WARN expected `heap_size` to be provided by Salsa query `GenericAlias < 'db >::variance_of_`
- WARN expected `heap_size` to be provided by Salsa query `GenericAlias < 'db >::variance_of_`
- WARN expected `heap_size` to be provided by Salsa query `GenericAlias < 'db >::variance_of_`
- WARN expected `heap_size` to be provided by Salsa query `ClassLiteral < 'db >::variance_of_`
- WARN expected `heap_size` to be provided by Salsa query `GenericAlias < 'db >::variance_of_`
- WARN expected `heap_size` to be provided by Salsa query `GenericAlias < 'db >::variance_of_`

@11happy
Copy link
Contributor Author

11happy commented Nov 2, 2025

Hii @ntBre would appreciate your views on some design decisions for this PR ,

  • I am currently detecting return statements like this,
    body.iter().any(|stmt| matches!(stmt, Stmt::Return(_)))
    
    I think it migh miss the nested ones, should I also use visitor pattern like in original B901
  • also if you have any suggestions for the error message, currently kept it short.
  • right now its pointing to yield should it point to return ?

Thank you : )

@github-actions
Copy link
Contributor

github-actions bot commented Nov 2, 2025

ruff-ecosystem results

Linter (stable)

✅ ecosystem check detected no linter changes.

Linter (preview)

ℹ️ ecosystem check detected linter changes. (+35 -0 violations, +0 -0 fixes in 4 projects; 51 projects unchanged)

binary-husky/gpt_academic (+2 -0 violations, +0 -0 fixes)

ruff check --no-cache --exit-zero --no-fix --output-format concise --preview

+ crazy_functions/paper_fns/auto_git/query_analyzer.py:292:13: B901 Using `yield` and `return {value}` in a generator function can lead to confusing behavior
+ crazy_functions/review_fns/query_analyzer.py:380:13: B901 Using `yield` and `return {value}` in a generator function can lead to confusing behavior

agronholm/anyio (+1 -0 violations, +0 -0 fixes)

ruff check --no-cache --exit-zero --no-fix --output-format concise --preview

+ src/anyio/pytest_plugin.py:145:5: B901 Using `yield` and `return {value}` in a generator function can lead to confusing behavior

python-trio/trio (+1 -0 violations, +0 -0 fixes)

ruff check --no-cache --exit-zero --no-fix --output-format concise --preview

+ src/trio/_core/_traps.py:69:5: B901 Using `yield` and `return {value}` in a generator function can lead to confusing behavior

pytest-dev/pytest (+31 -0 violations, +0 -0 fixes)

ruff check --no-cache --exit-zero --no-fix --output-format concise --preview

+ src/_pytest/assertion/__init__.py:192:9: B901 Using `yield` and `return {value}` in a generator function can lead to confusing behavior
+ src/_pytest/cacheprovider.py:298:9: B901 Using `yield` and `return {value}` in a generator function can lead to confusing behavior
+ src/_pytest/cacheprovider.py:419:9: B901 Using `yield` and `return {value}` in a generator function can lead to confusing behavior
+ src/_pytest/cacheprovider.py:461:9: B901 Using `yield` and `return {value}` in a generator function can lead to confusing behavior
+ src/_pytest/capture.py:890:9: B901 Using `yield` and `return {value}` in a generator function can lead to confusing behavior
+ src/_pytest/capture.py:895:13: B901 Using `yield` and `return {value}` in a generator function can lead to confusing behavior
+ src/_pytest/capture.py:900:13: B901 Using `yield` and `return {value}` in a generator function can lead to confusing behavior
+ src/_pytest/capture.py:905:13: B901 Using `yield` and `return {value}` in a generator function can lead to confusing behavior
+ src/_pytest/config/__init__.py:1412:13: B901 Using `yield` and `return {value}` in a generator function can lead to confusing behavior
+ src/_pytest/debugging.py:308:9: B901 Using `yield` and `return {value}` in a generator function can lead to confusing behavior
+ src/_pytest/faulthandler.py:102:9: B901 Using `yield` and `return {value}` in a generator function can lead to confusing behavior
+ src/_pytest/helpconfig.py:152:5: B901 Using `yield` and `return {value}` in a generator function can lead to confusing behavior
+ src/_pytest/logging.py:780:17: B901 Using `yield` and `return {value}` in a generator function can lead to confusing behavior
+ src/_pytest/logging.py:788:17: B901 Using `yield` and `return {value}` in a generator function can lead to confusing behavior
+ src/_pytest/logging.py:801:17: B901 Using `yield` and `return {value}` in a generator function can lead to confusing behavior
+ src/_pytest/logging.py:873:17: B901 Using `yield` and `return {value}` in a generator function can lead to confusing behavior
+ src/_pytest/pytester.py:171:13: B901 Using `yield` and `return {value}` in a generator function can lead to confusing behavior
+ src/_pytest/setuponly.py:36:9: B901 Using `yield` and `return {value}` in a generator function can lead to confusing behavior
+ src/_pytest/skipping.py:268:9: B901 Using `yield` and `return {value}` in a generator function can lead to confusing behavior
+ src/_pytest/skipping.py:312:5: B901 Using `yield` and `return {value}` in a generator function can lead to confusing behavior
+ src/_pytest/terminal.py:717:9: B901 Using `yield` and `return {value}` in a generator function can lead to confusing behavior
+ src/_pytest/terminal.py:981:9: B901 Using `yield` and `return {value}` in a generator function can lead to confusing behavior
+ src/_pytest/terminal.py:992:13: B901 Using `yield` and `return {value}` in a generator function can lead to confusing behavior
+ src/_pytest/tmpdir.py:315:5: B901 Using `yield` and `return {value}` in a generator function can lead to confusing behavior
+ src/_pytest/unittest.py:587:9: B901 Using `yield` and `return {value}` in a generator function can lead to confusing behavior
+ src/_pytest/warnings.py:109:9: B901 Using `yield` and `return {value}` in a generator function can lead to confusing behavior
+ src/_pytest/warnings.py:118:9: B901 Using `yield` and `return {value}` in a generator function can lead to confusing behavior
+ src/_pytest/warnings.py:128:9: B901 Using `yield` and `return {value}` in a generator function can lead to confusing behavior
+ src/_pytest/warnings.py:89:9: B901 Using `yield` and `return {value}` in a generator function can lead to confusing behavior
+ src/_pytest/warnings.py:98:9: B901 Using `yield` and `return {value}` in a generator function can lead to confusing behavior
+ testing/conftest.py:91:5: B901 Using `yield` and `return {value}` in a generator function can lead to confusing behavior

Changes by rule (1 rules affected)

code total + violation - violation + fix - fix
B901 35 35 0 0 0

Formatter (stable)

✅ ecosystem check detected no format changes.

Formatter (preview)

✅ ecosystem check detected no format changes.

@ntBre
Copy link
Contributor

ntBre commented Nov 3, 2025

Hii @ntBre would appreciate your views on some design decisions for this PR ,

  • I am currently detecting return statements like this,

    body.iter().any(|stmt| matches!(stmt, Stmt::Return(_)))
    

    I think it migh miss the nested ones, should I also use visitor pattern like in original B901

  • also if you have any suggestions for the error message, currently kept it short.

  • right now its pointing to yield should it point to return ?

Thank you : )

Yes, I think we should use a visitor like in B901. We can't fully replace B901 since it also applies to sync functions, but we should make sure we catch all of the async cases B901 handles.

I think the error message looks good currently and matches CPython, but I think it should point to return rather than yield, also like CPython and B901.

Signed-off-by: 11happy <soni5happy@gmail.com>
Signed-off-by: 11happy <soni5happy@gmail.com>
@11happy 11happy force-pushed the return_in_async_generator branch from 44b4bbe to 91d57b1 Compare November 7, 2025 03:18
@11happy 11happy marked this pull request as ready for review November 7, 2025 03:18
@11happy
Copy link
Contributor Author

11happy commented Nov 7, 2025

its ready for review : )
Thank you

@MichaReiser MichaReiser requested review from ntBre and removed request for AlexWaygood, carljm, dcreager, dhruvmanila and sharkdp November 10, 2025 12:22
Copy link
Contributor

@ntBre ntBre left a comment

Choose a reason for hiding this comment

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

Thanks! I think we could simplify things slightly by following the B901 implementation more closely.

@ntBre ntBre added the internal An internal refactor or improvement label Nov 10, 2025
@ntBre
Copy link
Contributor

ntBre commented Nov 10, 2025

Oh, one other thing I forgot to mention: I think we need to make B901 only run on sync generators and this syntax error only run on async generators. Then we'll need to convert the syntax error into a B901 error for now to preserve backwards compatibility.

Signed-off-by: 11happy <soni5happy@gmail.com>
Copy link
Contributor

@ntBre ntBre left a comment

Choose a reason for hiding this comment

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

Thanks, this is looking great. I just had a few more nits.

Signed-off-by: 11happy <bhuminjaysoni@gmail.com>
Signed-off-by: 11happy <bhuminjaysoni@gmail.com>
@11happy 11happy requested a review from ntBre November 26, 2025 11:29
Copy link
Contributor

@ntBre ntBre left a comment

Choose a reason for hiding this comment

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

Thank you, this is looking great! I just had one more nit about test placement, and I noticed a preexisting error that I think we can fix here too.

11happy and others added 6 commits December 2, 2025 10:41
Signed-off-by: 11happy <bhuminjaysoni@gmail.com>
Signed-off-by: 11happy <bhuminjaysoni@gmail.com>
Signed-off-by: 11happy <bhuminjaysoni@gmail.com>
Signed-off-by: 11happy <bhuminjaysoni@gmail.com>
@amyreese amyreese requested a review from ntBre December 2, 2025 23:14
ntBre added 4 commits December 2, 2025 18:17
these test cases were labeled as `not_broken`, but pasting them into the REPL as
async functions shows that they are all syntax errors:

```py
>>> async def not_broken6():
...     return (yield from [])
...
  File "<python-input-1>", line 2
    return (yield from [])
    ^^^^^^^^^^^^^^^^^^^^^^
SyntaxError: 'return' with value in async generator
>>> async def not_broken7():
...     x = yield from []
...     return x
...
  File "<python-input-3>", line 2
    x = yield from []
        ^^^^^^^^^^^^^
SyntaxError: 'yield from' inside async function
>>> async def not_broken8():
...     x = None
...
...     def inner(ex):
...         nonlocal x
...         x = ex
...
...     inner((yield from []))
...     return x
...
...
  File "<python-input-4>", line 8
    inner((yield from []))
           ^^^^^^^^^^^^^
SyntaxError: 'yield from' inside async function
```

so I think it's correct to flag the sync versions too. I'll update the names in
the next commit
Copy link
Contributor

@ntBre ntBre left a comment

Choose a reason for hiding this comment

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

Thanks! I pushed a few commits. Just waiting for CI and then I'll merge!

@ntBre
Copy link
Contributor

ntBre commented Dec 3, 2025

Hmm, there are actually 35 new ecosystem hits. They all look like true positives to me, and this is a preview rule, so I think it's okay to accept this, but @amyreese, do you know if code like this is common enough that we should allow it?

@pytest.hookimpl(wrapper=True, tryfirst=True)
def pytest_collection(session: Session) -> Generator[None, object, object]:
    config = session.config
    with catch_warnings_for_item(
        config=config, ihook=config.hook, when="collect", item=None
    ):
        return (yield)  # <--

It popped up many times in pytest, but maybe they just wouldn't want to enable this rule.

It seems to me that the motivation for the rule applies whether the yield is a standalone expression or not. But we should probably include this in the release notes, either by splitting out the visitor change/bug fix or at least by updating the title and labels.

For a bit more context, my suggestion to start flagging these was in #21200 (comment).

@amyreese
Copy link
Member

amyreese commented Dec 3, 2025

I've literally never seen return (yield) in the wild before today. 😅

Since most of the ecosystem hits seem related to pytest hooks, I think it would be ok as-is — it's a really weird pattern and seems to negate most of the "purpose" of a generator function.

I presume that pytest is doing something like sending a value back into a generator and then capturing that in the StopIteration when it returns, and I suspect they would already have B901 disabled.

@amyreese
Copy link
Member

amyreese commented Dec 3, 2025

2.2k hits on github, some of which at least seem mildly more useful than what I saw in pytest: https://github.com/search?q=%22return+%28yield%29%22&type=code

I can't imagine people are actually using the actual value from their yield expressions as a return value, probably more as shorthand for yield X; return.

I guess as exceptions to a rule go, it's not crazy to allow it, just weird.

@ntBre
Copy link
Contributor

ntBre commented Dec 3, 2025

Thanks! Sounds like it's probably reasonable to land this, especially since the rule is still in preview. I'll try to update the title and labels so that it shows up in the release notes.

@ntBre ntBre changed the title [syntax-errors]: semantic syntax error for async generator with return [flake8-bugbear] Catch yield expressions within other statements (B901) Dec 3, 2025
@ntBre ntBre added rule Implementing or modifying a lint rule preview Related to preview mode features and removed internal An internal refactor or improvement labels Dec 3, 2025
@codspeed-hq
Copy link

codspeed-hq bot commented Dec 3, 2025

CodSpeed Performance Report

Merging #21200 will not alter performance

Comparing 11happy:return_in_async_generator (fe9fb9a) with main (4488e9d)

Summary

✅ 52 untouched

@ntBre
Copy link
Contributor

ntBre commented Dec 3, 2025

Hmm, these benchmarks don't seem right. I'll try to merge main again.

@AlexWaygood
Copy link
Member

Hmm, these benchmarks don't seem right. I'll try to merge main again.

The pydantic benchmark seems to fairly regularly vary around 5% or so since we landed #21467, unfortunately :/

@ntBre ntBre merged commit c722f49 into astral-sh:main Dec 3, 2025
38 checks passed
@ntBre
Copy link
Contributor

ntBre commented Dec 3, 2025

Hmm maybe I should have searched around a bit more. I stumbled across #14453 and #15101 and #15254 for unrelated reasons this afternoon. Apparently this change has already been considered and rejected.

I still think it makes sense to flag these, including after checking the upstream implementation (#21200 (comment)), but I guess we should either revert the visitor changes (not the syntax error part) like #15254 or close #14453 as now completed. As I say in my other comment, we at least have to visit sub-expressions in the async case because that's a syntax error:

>>> async def foo():
...     return (yield 1)
...
  File "<python-input-0>", line 2
    return (yield 1)
    ^^^^^^^^^^^^^^^^
SyntaxError: 'return' with value in async generator

but I think that bolsters the sync case too.

cc @MichaReiser who reviewed the earlier PR

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

preview Related to preview mode features rule Implementing or modifying a lint rule

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants