diff --git a/Lib/asyncio/tasks.py b/Lib/asyncio/tasks.py index 14d3c1ceb58cac..9d20930dc300c6 100644 --- a/Lib/asyncio/tasks.py +++ b/Lib/asyncio/tasks.py @@ -524,6 +524,7 @@ def _on_completion(f): timeout_handle.cancel() for f in fs: f.remove_done_callback(_on_completion) + futures.future_discard_from_awaited_by(f, cur_task) done, pending = set(), set() for f in fs: diff --git a/Lib/test/test_asyncio/test_tasks.py b/Lib/test/test_asyncio/test_tasks.py index 7a217f886b87de..609ccdbfdb2b28 100644 --- a/Lib/test/test_asyncio/test_tasks.py +++ b/Lib/test/test_asyncio/test_tasks.py @@ -1218,6 +1218,19 @@ def gen(): loop.advance_time(10) loop.run_until_complete(asyncio.wait([a, b])) + def test_wait_discards_awaited_by_for_pending(self): + # gh-152569: wait() must remove itself from the await-graph of every + # future once it returns, including futures that never resolved. + async def coro(): + immortal = self.loop.create_future() + done = self.new_task(self.loop, asyncio.sleep(0)) + await asyncio.wait({done, immortal}, + return_when=asyncio.FIRST_COMPLETED) + self.assertFalse(immortal._asyncio_awaited_by) + immortal.cancel() + + self.loop.run_until_complete(self.new_task(self.loop, coro())) + def test_wait_really_done(self): # there is possibility that some tasks in the pending list # became done but their callbacks haven't all been called yet diff --git a/Misc/NEWS.d/next/Library/2026-06-29-09-45-00.gh-issue-152569.Kf7Lq2.rst b/Misc/NEWS.d/next/Library/2026-06-29-09-45-00.gh-issue-152569.Kf7Lq2.rst new file mode 100644 index 00000000000000..fb7e32a5260b39 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-06-29-09-45-00.gh-issue-152569.Kf7Lq2.rst @@ -0,0 +1,3 @@ +Fix :func:`asyncio.wait` leaking waiting tasks via the await-graph when racing a +future that never resolves. The waiting task is now discarded from every future's +``awaited_by`` set once :func:`~asyncio.wait` returns, even for pending futures.