PEP 492 vs. PEP 3152, new round

From: Guido van Rossum <guido-AT-python.org> To: Yury Selivanov <yselivanov.ml-AT-gmail.com>, Greg Ewing <greg.ewing-AT-canterbury.ac.nz>, Python-Dev <python-dev-AT-python.org>, Victor Stinner <victor.stinner-AT-gmail.com> Subject: PEP 492 vs. PEP 3152, new round Date: Fri, 24 Apr 2015 10:03:48 -0700 Message-ID: <CAP7+vJK6GQ=rJ__KeE=H1cAES3gThdg4okmq+iFFSvE6SushBw@mail.gmail.com> Archive-link: Article, Thread

I've tried to catch up with the previous threads. A summary of issues brought up: 1. precise syntax of `async def` (or do we need it at all) 2. do we need `async for` and `async with` (and how to spell them) 3. syntactic priority of `await` 4. `cocall` vs. `await` 5. do we really need `__aiter__` and friends 6. StopAsyncException 7. compatibility with asyncio and existing users of it (I've added a few myself.) I'll try to take them one by one. *1. precise syntax of `async def`* Of all the places to put `async` I still like *before* the `def` the best. I often do "imprecise search" for e.g. /def foo/ and would be unhappy if this didn't find async defs. Putting it towards the end (`def foo async()` or `def foo() async`) makes it easier to miss. A decorator makes it hard to make the syntactic distinctions required to reject `await` outside an async function. So I still prefer *`async def`*. *2. do we need `async for` and `async with`* Yes we do. Most of you are too young to remember, but once upon a time you couldn't loop over the lines of a file with a `for` loop like you do now. The amount of code that was devoted to efficiently iterate over files was tremendous. We're back in that stone age with the asyncio `StreamReader` class -- it supports `read()`, `readline()` and so on, but you can't use it with `for`, so you have to write a `while True` loop. `asyncio for` makes it possible to add a simple `__anext__` to the `StreamReader` class, as follows: ``` async def __anext__(self): line = await self.readline() if not line: raise StopAsyncIteration return line ``` A similar argument can be made for `async with`; the transaction commit is pretty convincing, but it also helps to be able to wait e.g. for a transport to drain upon closing a write stream. As for how to spell these, I think having `async` at the front makes it most clear that this is a special form. (Though maybe we should consider `await for` and `await with`? That would have the advantage of making it easy to scan for all suspension points by searching for /await/. But being a verb it doesn't read very well.) *3. syntactic priority of `await`* Yury, could you tweak the syntax for `await` so that we can write the most common usages without parentheses? In particular I'd like to be able to write ``` return await foo() with await foo() as bar: ... foo(await bar(), await bletch()) ``` (I don't care about `await foo() + await bar()` but it would be okay.) ``` I think this is reasonable with some tweaks of the grammar (similar to what Greg did for cocall, but without requiring call syntax at the end). *4. `cocall` vs. `await`* Python evolves. We couldn't have PEP 380 (`yield from`) without prior experience with using generators as coroutines (PEP 342), which in turn required basic generators (PEP 255), and those were a natural evolution of Python's earlier `for` loop. We couldn't PEP 3156 (asyncio) without PEP 380 and all that came before. The asyncio library is getting plenty of adoption and it has the concept of separating the *getting* of a future[1] from *waiting* for it. IIUC this is also how `await` works in C# (it just requires something with an async type). This has enabled a variety of operations that take futures and produce more futures. [1] I write `future` with a lowercase 'f' to include concepts like coroutine generator objects. *I just can't get used to this aspect of PEP 3152, so I'm rejecting it.* Sorry Greg, but that's the end. We must see `await` as a refinement of `yield from`, not as an alternative. (Yury: PEP 492 is not accepted yet, but you're getting closer.) One more thing: this separation is "Pythonic" in the sense that it's similar to the way *getting* a callable object is a separate act from *calling* it. While this is a cause for newbie bugs (forgetting to call an argument-less function) it has also enabled the concept of "callable" as more general and more powerful in Python: any time you need to pass a callable, you can pass e.g. a bound method or a class or something you got from `functools.partial`, and that's a useful thing (other languages require you to introduce something like a lambda in such cases, which can be painful if the thing you wrap has a complex signature -- or they don't support function parameters at all, like Java). I know that Greg defends it by explaining that `cocal f(args)` is not a `cocall` operator applied to `f(args)`, it is the *single* operator `cocall ...(args)` applied to `f`. But this is too subtle, and it just doesn't jive with the long tradition of using `yield from f` where f is some previously obtained future. *5. do we really need `__aiter__` and friends* There's a lot of added complexity, but I think it's worth it. I don't think we need to make the names longer, the 'a' prefix is fine for these methods. I think it's all in the protocols: regular `with` uses `__enter__` and `__exit__`; `async with` uses `__aenter__` and `__aexit__` (which must return futures). Ditto for `__aiter__` and `__anext__`. I guess this means that the async equivalent to obtaining an iterator through `it = iter(xs)` followed by `for x over it` will have to look like `ait = await aiter(xs)` followed by `for x over ait`, where an iterator is required to have an `__aiter__` method that's an async function and returns self immediately. But what if you left out the `await` from the first call? I.e. can this work? ``` ait = aiter(xs) async for x in ait: print(x) ``` The question here is whether the object returned by aiter(xs) has an `__aiter__` method. Since it was intended to be the target of `await`, it has an `__await__` method. But that itself is mostly an alias for `__iter__`, not `__aiter__`. I guess it can be made to work, the object just has to implement a bunch of different protocols. *6. StopAsyncException* I'm not sure about this. The motivation given in the PEP seems to focus on the need for `__anext__` to be async. But is this really the right pattern? What if we required `ait.__anext__()` to return a future, which can either raise good old `StopIteration` or return the next value from the iteration when awaited? I'm wondering if there are a few alternatives to be explored around the async iterator protocol still. *7. compatibility with asyncio and existing users of it* This is just something I want to stress. On the one hand it should be really simple to take code written for pre-3.5 asyncio and rewrite it to PEP 492 -- simple change `@asyncio.coroutine` to `async def` and change `yield from` to `await`. (Everything else is optional; existing patterns for loops and context managers should continue to work unchanged, even if in some cases you may be able to simplify the code by using `async for` and `async with`.) But it's also important that *even if you don't switch* (i.e. if you want to keep your code compatible with pre-3.5 asyncio) you can still use the PEP 492 version of asyncio -- i.e. the asyncio library that comes with 3.5 must seamlessly support mixing code that uses `await` and code that uses `yield from`. And this should go both ways -- if you have some code that uses PEP 492 and some code that uses pre-3.5 asyncio, they should be able to pass their coroutines to each other and wait for each other's coroutines. That's all I have for now. Enjoy! -- --Guido van Rossum (python.org/~guido)