Mypy: Unexpected behavior regarding async generator

Created on 17 May 2018  路  5Comments  路  Source: python/mypy

Trying to get typing work in an asynchronous code and I don't understand mypy's behavior.

Let's say we have two coroutines typed in the following way:

from typing import AsyncGenerator

async def gen1() -> AsyncGenerator[str, None]:
    pass
reveal_type(gen1)

async def gen2() -> AsyncGenerator[str, None]:
    yield 'tick'
reveal_type(gen2)

mypy:

$ mypy file.py
file.py:5: error: Revealed type is 'def () -> typing.Awaitable[typing.AsyncGenerator[builtins.str, builtins.None]]'
file.py:9: error: Revealed type is 'def () -> typing.AsyncGenerator[builtins.str, builtins.None]'

So the type of the first one gets wrapped in Awaitable. I don't fully understand why. I thought all Async... are Awaitables by definition. I did not find anything about it but here #3576, which is not enough for me to get it.

The same thing in case of methods:

class BaseClass:
    async def gen1(self) -> AsyncGenerator[str, None]:
        pass
reveal_type(BaseClass.gen1)

class MyClass(BaseClass):
    async def gen1(self) -> AsyncGenerator[str, None]:
        yield 'tick'
reveal_type(MyClass.gen1)

mypy:

file.py:4: error: Revealed type is 'def (self: acm.workers.BaseClass) -> typing.Awaitable[typing.AsyncGenerator[builtins.str, builtins.None]]'
file.py:7: error: Return type of "gen1" incompatible with supertype "BaseClass"
file.py:9: error: Revealed type is 'def (self: acm.workers.MyClass) -> typing.AsyncGenerator[builtins.str, builtins.None]'

So now I cannot define abstract methods that are async generators, in a straightforward way.

Versions

Python 3.6.5
mypy==0.600

EDIT: I fixed a random typo @JelleZijlstra found and edited parts of the text based on the typo. Now the whole problem is clearer.

documentation topic-usability

Most helpful comment

The if False: yield trick is as old as Python probably is. I think the source of confusion here may be that type annotations are _completely ignored_ by Python runtime. Just by typing something as a generator it will not magically become a generator, you still need a yield somewhere.

There is unfortunately no simple solution, we may just document this in common issues.

All 5 comments

Your gen1 is not a generator (it has no yield), so mypy interprets it as a normal async function that returns an AsyncGenerator object. gen2 is actually correctly typed as def () -> typing.AsyncGenerator[builtins.str, builtins.None], but your second reveal_type has a typo.

The issue with overrides can be explained by the same logic: the base class's gen1 is not a generator because there's no yield. You can probably work around it by adding a dummy yield inside the function or by removing the async from its def (I haven't tested), but it does seem like a usability issue that the obvious way to type an abstract AsyncGenerator doesn't work.

The if False: yield trick is as old as Python probably is. I think the source of confusion here may be that type annotations are _completely ignored_ by Python runtime. Just by typing something as a generator it will not magically become a generator, you still need a yield somewhere.

There is unfortunately no simple solution, we may just document this in common issues.

Thank you both, now I get it. My typo (which I will correct in a second) only added to my confusion. I agree that this probably should be explained in docs somewhere.

Update: I started to use plain yield in those kind of abstract async generators and I can say I like it: it's just as succinct as pass (which is, in this context, equivalent to plain return) and informative for the user as well.

I too like the plain yield but think it needs to be documented, it took a bit of googling to find this page!

Was this page helpful?
0 / 5 - 0 ratings