Skip to content

fix: correct sync iterator type of asyncio.as_completed to yield Coroutine#15672

Open
armorbreak001 wants to merge 2 commits intopython:mainfrom
armorbreak001:fix/asyncio-as-completed-sync-iterator
Open

fix: correct sync iterator type of asyncio.as_completed to yield Coroutine#15672
armorbreak001 wants to merge 2 commits intopython:mainfrom
armorbreak001:fix/asyncio-as-completed-sync-iterator

Conversation

@armorbreak001
Copy link
Copy Markdown

@armorbreak001 armorbreak001 commented Apr 24, 2026

Summary

Fixes #15646as_completed() sync iteration yields Coroutine, not Future.

Problem

When iterating over asyncio.as_completed() with a regular for loop (not async for), it yields coroutines at runtime. However, the stub typed the sync iterator as Iterator[Future[_T]], which allows calling .result() without a type error — but this crashes at runtime with AttributeError: 'coroutine' object has no attribute 'result'.

Confirmed on CPython 3.12:

>>> import asyncio
>>> it = asyncio.as_completed([asyncio.sleep(0)])
>>> type(iter(it).__next__())
<class 'coroutine'>

Changes

stdlib/asyncio/tasks.pyi:

Version Before After
≥3.13 _SyncAndAsyncIterator[Future[_T]] (sync=Iterator[Future]) _SyncAndAsyncIterator[_T] where sync path is Iterator[Coroutine[Any, Any, _T]]
3.10–3.12 Iterator[Future[_T]] Unchanged (kept for backward compat)
<3.10 Iterator[Future[_T]] (with loop=) Unchanged

The 3.13+ _SyncAndAsyncIterator Protocol now correctly reflects:

  • Sync (__iter__): yields Coroutine[Any, Any, T]
  • Async (__aiter__): yields Future[T]

Scope Limitation

The fix is limited to Python 3.13+ only. While the same runtime behavior exists in older versions, changing those stubs breaks existing code (confirmed via mypy_primer) that assigns sync-iter results to Future/Task annotated variables — even though such code would crash at runtime when calling .result(). The typeshed team can decide whether to accept that breakage in a future major version bump.

Verification

  • Pyright: 0 errors on modified file
  • Runtime confirmed: iter(as_completed(...)).__next__() returns coroutine

…utine

When as_completed() is iterated with a regular for loop (not async
for), it yields coroutines, not Futures. The previous stub incorrectly
typed the sync iterator path as Iterator[Future[_T]] which would
allow calling .result() without a type error even though that crashes
at runtime with AttributeError.

Changes:
- 3.13+: _SyncAndAsyncIterator now extends
  Iterator[Coroutine[Any, Any, _T]] (sync) + AsyncIterator[Future[_T]] (async)
- 3.10-3.12: Return type changed to Iterator[Coroutine[Any, Any, _T]]
- <3.10: Same correction for legacy loop= variant

Fixes python#15646
@github-actions

This comment has been minimized.

Revert changes to <3.13 stubs to avoid breaking existing code that
assigns as_completed() sync-iter results to Future/Task variables.
The Coroutine type is correct at runtime but too breaking for older versions.

Only 3.13+ gets the fix where _SyncAndAsyncIterator Protocol exists.
@github-actions
Copy link
Copy Markdown
Contributor

Diff from mypy_primer, showing the effect of this PR on open source code:

pandas (https://github.com/pandas-dev/pandas)
- pandas/core/computation/ops.py:328: error: Need type annotation for "_binary_ops_dict" (hint: "_binary_ops_dict: dict[<type>, <type>] = ...")  [var-annotated]

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.

asyncio.as_completed is incorrectly typed as yielding Futures when used as a regular iterator

1 participant