This issue tracker has been migrated to GitHub, and is currently read-only.
For more information, see the GitHub FAQs in the Python's Developer Guide.

classification
Title: asyncio.wait loses coroutine return value
Type: Stage: resolved
Components: asyncio Versions: Python 3.5
process
Status: closed Resolution: wont fix
Dependencies: Superseder:
Assigned To: Nosy List: André Caron, asvetlov, gvanrossum, vstinner, yselivanov
Priority: normal Keywords:

Created on 2016-02-13 21:17 by André Caron, last changed 2022-04-11 14:58 by admin. This issue is now closed.

Files
File name Uploaded Description Edit
asyncio-wait-coroutine.py André Caron, 2016-02-13 21:17 Demonstration of problem.
Messages (5)
msg260250 - (view) Author: André Caron (André Caron) Date: 2016-02-13 21:17
When the asyncio.wait() function with coroutine objects as inputs, it is impossbible to extract the results reliably.

This issue arises because asyncio.wait() returns an unordered set of futures against which membership tests of coroutine objects always fail.

The issue is made even worse by the fact that coroutine objects cannot be awaited multiple times (see https://bugs.python.org/issue25887).  Any await expression on the coroutine object after the call to asyncio.wait() returns None, regardless of the coroutine's return value.

  (See attached `asyncio-wait-coroutine.py` for an example of both these issues.)

In the worst case, multiple inputs are coroutine objects and the set of returned futures contains return values but there is no way to determine which result corresponds to which coroutine call.

To work around this issue, callers need to explicitly use asyncio.ensure_future() on coroutine objects before calling asyncio.wait().

  (See comment in `asyncio-wait-coroutine.py` for an example "fix").

Note that, in general, it is not possible to know ahead of time whether all inputs to asyncio.wait() are coroutines or futures.  Furthermore, the fact that a given third-party library function is implemented as a regular function that returns a Future or a proper coroutine is an implementation decision which may not be part of the public interface.  Even if it is, the inputs to asyncio.wait() may come from complex code paths and it may be difficult to verify that all of them end up producing a Future.

As a consequence, the only reliable way to recover all results from asyncio.wait() is to explicitly call asyncio.ensure_future() on each of the inputs.

When doing so, both the membership test against the `done` set and the await expressions work as expected.

Quickly, there are several possible solutions:
- allow programs to await coroutine multiple times;
- make the set membership test of a coroutine object succeed; or
- change support for coroutine objects as inputs to asyncio.wait():
** update documentation for asyncio.wait() to explain this limitation; or
** explicitly reject coroutine objects; or
** warn when passing coroutine objects as inputs -- unless wrapped.

Related issue: https://bugs.python.org/issue25887 proposes a patch to change the behaviour of awaiting a coroutine object multiple times in order to produce a RuntimeError on all awaits after the 1st.  While that change in behaviour would make it easier to diagnose the loss of the return value, it does not fix this issue.
msg260257 - (view) Author: Guido van Rossum (gvanrossum) * (Python committer) Date: 2016-02-14 00:20
I wonder if the right solution isn't to insist that the arguments to await() are Futures, not coroutines. While in many cases (e.g. gather(), wait_for()) it's a useful convention to support either coroutines or Futures, for wait() it does seem pretty useless. Mapping the Futures back to coroutines isn't a good solution either, since you can't get the result out of just the coroutine.

The question is whether we can just change this or whether we should issue some kind of deprecation warning, since it's at least conceivable that someone relies on the current behavior and doesn't care about mapping results back to coroutines (e.g. if the results are self-identifying).

The same issue applies to as_completed(), I think.
msg260325 - (view) Author: André Caron (André Caron) Date: 2016-02-15 18:03
Hi Guido,

Thanks for the quick reply :-)

AFAICT, there seem to be three possible directions regarding this issue -- for both wait() and as_completed():

1) remove the need for ensure_future(): make the membership test succeed and allow multiple await expressions on the same coroutine;

2) fail fast: reject coroutine objects as inputs to wait() and reject multiple await expressions on coroutine objects (see issue #25887);

3) clarify API usage: deprecate couroutine objects and enhance docs for wait(), as_completed() and ensure_future().

From a pure API standpoint, #1 makes the API more uniform and less surprising, #2 makes it easier to detect and fix incorrect usage and #3 accelerates troubleshooting when people get bitten by this quirk in the API.

You're right that technically, the case of side-effect-only coroutine invocations is a use case that's currently supported by wait() and that removing this (e.g. by adopting #2) would be a compatibility break.  I personally doubt that this is a common use case as both wait() and as_completed() seem specifically designed to recover the results, but I'm not an asyncio expert either.

Asyncio is still young and is undergoing adoption, so I would like to see this issue resolved without resorting to #3.

Any chance #1 or #2 can be considered?
msg260327 - (view) Author: Yury Selivanov (yselivanov) * (Python committer) Date: 2016-02-15 18:22
TBH I never ever needed to do membership tests on (done, failed) result of asyncio.wait.  If you need to do such tests - just wrap your coroutines into tasks manually.  I honestly don't understand what's the problem and why we need to change anything in asyncio or in Python.  There're tons of code on asyncio now, and this is only a second time someone wants to "fix" await.

Fixing #25887 allows us to enable multiple awaits on coroutines later, but I wouldn't rush that in 3.5 or 3.6.

Restricting asyncio.wait to accept only futures will also cause a lot of pain.  I'd just fix the docs with an explanation of this problem and with a snippet of code showing how to do membership tests if needed.  Alternatively, we can add a wrapper for asyncio.wait (wait_futures?), that will only accept futures/tasks.
msg308861 - (view) Author: Andrew Svetlov (asvetlov) * (Python committer) Date: 2017-12-21 09:24
Let's close as "wont fix".
If user need an identity for awaited coroutine -- he/she should return it as part of coroutine result.
History
Date User Action Args
2022-04-11 14:58:27adminsetgithub: 70545
2017-12-21 09:24:04asvetlovsetstatus: open -> closed

nosy: + asvetlov
messages: + msg308861

resolution: wont fix
stage: resolved
2016-02-15 18:22:43yselivanovsetmessages: + msg260327
2016-02-15 18:03:37André Caronsetmessages: + msg260325
2016-02-14 00:20:10gvanrossumsetmessages: + msg260257
2016-02-13 21:17:53André Caroncreate