From 104b39687b31e47b0d98d2c219a37ae046b7e3de Mon Sep 17 00:00:00 2001 From: A5rocks Date: Thu, 23 Jul 2026 02:10:36 +0900 Subject: [PATCH 1/3] Special case Task Groups and their special functions --- mypy/checker.py | 28 ++++++++++++++++++----- test-data/unit/check-async-await.test | 33 +++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 6 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 36c9377af0bc8..5f5e604789b7e 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -5160,12 +5160,14 @@ def try_infer_partial_type_from_indexed_assignment( new_type = self.named_generic_type(typename, [key_type, value_type]) self.replace_partial_type(var, new_type, partial_types) - def type_requires_usage(self, typ: Type) -> tuple[str, ErrorCode] | None: - """Some types require usage in all cases. The classic example is - an unused coroutine. + def type_requires_usage(self, typ: Type, s: ExpressionStmt) -> tuple[str, ErrorCode] | None: + """Some types require usage in basically all cases. The classic + example is an unused coroutine. In the case that it does require usage, returns a note to attach - to the error message. + to the error message. We special case somethings that return + awaitables because in those particular cases we can guarantee + it's safe. """ proper_type = get_proper_type(typ) if isinstance(proper_type, Instance): @@ -5174,12 +5176,26 @@ def type_requires_usage(self, typ: Type) -> tuple[str, ErrorCode] | None: if proper_type.type.fullname == "typing.Coroutine": return ("Are you missing an await?", UNUSED_COROUTINE) if proper_type.type.get("__await__") is not None: - return ("Are you missing an await?", UNUSED_AWAITABLE) + # this is quite brittle, but there's no good way around this. + # called_on + if isinstance(s.expr, CallExpr) and isinstance(s.expr.callee, MemberExpr): + called_on = get_proper_type(self.expr_checker.accept(s.expr.callee.expr)) + is_a_taskgroup = ( + isinstance(called_on, Instance) and + (called_on.type.fullname, s.expr.callee.name) in [ + ("asyncio.taskgroups.TaskGroup", "create_task"), + ("anyio.abc._tasks.TaskGroup", "start_soon") + ] + ) + else: + is_a_taskgroup = False + if not is_a_taskgroup: + return ("Are you missing an await?", UNUSED_AWAITABLE) return None def visit_expression_stmt(self, s: ExpressionStmt) -> None: expr_type = self.expr_checker.accept(s.expr, allow_none_return=True, always_allow_any=True) - error_note_and_code = self.type_requires_usage(expr_type) + error_note_and_code = self.type_requires_usage(expr_type, s) if error_note_and_code: error_note, code = error_note_and_code self.fail( diff --git a/test-data/unit/check-async-await.test b/test-data/unit/check-async-await.test index cd40f2cf29a65..a469bc2be1c05 100644 --- a/test-data/unit/check-async-await.test +++ b/test-data/unit/check-async-await.test @@ -867,6 +867,39 @@ def f(c: C) -> None: [builtins fixtures/async_await.pyi] [typing fixtures/typing-async.pyi] +[case testOkUnusedAwaitable] +# flags: --python-version 3.11 --show-error-codes --enable-error-code unused-awaitable +import asyncio + +async def main() -> None: + async with asyncio.TaskGroup() as tg: + # this is ok: + tg.create_task(asyncio.sleep(1)) + + # but from something else (e.g. passed through a reveal_type) it's not ok + reveal_type(tg.create_task(asyncio.sleep(1))) # E: Value of type "Awaitable[object]" must be used [unused-awaitable] \ + # N: Are you missing an await? \ + # N: Revealed type is "typing.Awaitable[builtins.object]" + + + + + + +[file asyncio/__init__.pyi] +from .taskgroups import TaskGroup as TaskGroup + +async def sleep(time: int) -> None: pass + +[file asyncio/taskgroups.pyi] +from typing import Awaitable + +class TaskGroup: + def create_task(self, coro: object) -> Awaitable[object]: pass + + async def __aenter__(self) -> "TaskGroup": pass + async def __aexit__(self, x, y, z) -> None: pass + [case testAsyncForOutsideCoroutine] async def g(): yield 0 From 3216ecee62a240a933deec467274f1fd0d66bdc3 Mon Sep 17 00:00:00 2001 From: A5rocks Date: Thu, 23 Jul 2026 02:29:24 +0900 Subject: [PATCH 2/3] Make special case wider --- mypy/checker.py | 11 +++++------ test-data/unit/check-async-await.test | 4 ---- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 5f5e604789b7e..b5a652d48309d 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -5176,16 +5176,15 @@ def type_requires_usage(self, typ: Type, s: ExpressionStmt) -> tuple[str, ErrorC if proper_type.type.fullname == "typing.Coroutine": return ("Are you missing an await?", UNUSED_COROUTINE) if proper_type.type.get("__await__") is not None: - # this is quite brittle, but there's no good way around this. - # called_on + # this is quite ad-hoc, but there's no good way around + # this. the alternative is a hardcoded list of + # TaskGroups and their respective functions, but that's + # a lot of maintenance! if isinstance(s.expr, CallExpr) and isinstance(s.expr.callee, MemberExpr): called_on = get_proper_type(self.expr_checker.accept(s.expr.callee.expr)) is_a_taskgroup = ( isinstance(called_on, Instance) and - (called_on.type.fullname, s.expr.callee.name) in [ - ("asyncio.taskgroups.TaskGroup", "create_task"), - ("anyio.abc._tasks.TaskGroup", "start_soon") - ] + called_on.type.fullname.endswith(".TaskGroup") ) else: is_a_taskgroup = False diff --git a/test-data/unit/check-async-await.test b/test-data/unit/check-async-await.test index a469bc2be1c05..e6675e864db06 100644 --- a/test-data/unit/check-async-await.test +++ b/test-data/unit/check-async-await.test @@ -882,10 +882,6 @@ async def main() -> None: # N: Revealed type is "typing.Awaitable[builtins.object]" - - - - [file asyncio/__init__.pyi] from .taskgroups import TaskGroup as TaskGroup From 67445f81d8ad2a121235101f34e1d1fa182e2b22 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 22 Jul 2026 17:32:30 +0000 Subject: [PATCH 3/3] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mypy/checker.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index b5a652d48309d..813939ca49646 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -5182,10 +5182,9 @@ def type_requires_usage(self, typ: Type, s: ExpressionStmt) -> tuple[str, ErrorC # a lot of maintenance! if isinstance(s.expr, CallExpr) and isinstance(s.expr.callee, MemberExpr): called_on = get_proper_type(self.expr_checker.accept(s.expr.callee.expr)) - is_a_taskgroup = ( - isinstance(called_on, Instance) and - called_on.type.fullname.endswith(".TaskGroup") - ) + is_a_taskgroup = isinstance( + called_on, Instance + ) and called_on.type.fullname.endswith(".TaskGroup") else: is_a_taskgroup = False if not is_a_taskgroup: