Unravelling `async for` loops
When I decided the next post in my series on Python's syntactic sugar would be on async for
, I figured it would be straightforward. I have already done `for` loops, so I have something to build off of. The language reference also specifies the pseudo-code for async for
, so I really didn't have to think too much about what the unravelled form should be.
But then I decided I would break with my practice of using Python 3.8 as the reference version of Python I work from and instead pull from Python 3.10 due to two additions in that version: aiter()
and anext()
. That decision turned out to complicate my life a bit. 😉
The bytecode
First, though, let's look at the bytecode. Our running example will be:
The bytecode for such a block of code is:
The code for GET_AITER
shows that it:
- Calls
__aiter__()
onb
. - Checks that
__anext__()
is defined and is awaitable on what__aiter__()
returns.
The code for GET_ANEXT
shows that it:
- Checks if the iterator defines
__anext__()
and it is awaitable. - Pushes the awaitable on to the stack to be called via
YIELD_FROM
.
Everything else is about calling the awaitable, going through the loop, etc. It's a lot like for
, but subsitute __iter__
for __aiter__
and __next__
with __anext__
which also happens to be an awaitable.
But before we talk about how to translate an async for
statement to something lower level, we should talk about aiter()
and anext()
.
The Built-ins
aiter()
If you read my for
loop post and its section on iter()
you will notice that when it's given an iterable it calls __iter__()
on the object and checks that the iterator returned defines __next__()
. I figured aiter()
would be the similar and call __aiter__()
and check it defined an awaitable __anext__()
.
Turns out I was wrong (initially). Looking at CPython 3.10.0.rc1, the implementation of aiter()
calls PyObject_GetAiter()
. The code itself doesn't do anything fancy:
- Check if the iterable defines
__aiter__()
. - If so, call it.
- Check if the returned object is an async iterator via
PyAiter_Check()
.
That's what I was expecting. But then I checked what PyAiter_Check()
does, and that turned out it checked if the object defined __anext__()
and __aiter__()
. Huh. That's different from how iter()
works.
And so I asked why this discrepancy existed. Turned out there's wasn't a key reason to it, and so for Python 3.10.0rc2, aiter()
was changed to only require the returned async iterator from __aiter__()
to define __anext__()
, thus operating more like iter()
.
anext()
The implementation of anext()
is straightforward as it just calls __anext__()
.
The semantics
Unravelling async for
follows the format of for
loops very closely. The only key differences are:
- You call
aiter()
instead ofiter()
- You call
anext()
instead ofnext()
- You
await
onanext()
- You catch
StopAsyncIteration
instead ofStopIteration
Thanks to having worked out how aiter()
and anext()
work, we can take our for
loop examples and mechanically translate them to work for async for
.