Skip to content

Commit 7c49fa6

Browse files
authored
Improve async documentation (#14973)
Some of the examples here neither ran nor type checked. Remove mention and use of deprecated APIs. Also go into some detail about async generators. Document #5385 since it comes up not infrequently. Linking #13681
1 parent ddf0c05 commit 7c49fa6

File tree

1 file changed

+82
-64
lines changed

1 file changed

+82
-64
lines changed

docs/source/more_types.rst

+82-64
Original file line numberDiff line numberDiff line change
@@ -824,11 +824,11 @@ classes are generic, self-type allows giving them precise signatures:
824824
Typing async/await
825825
******************
826826

827-
Mypy supports the ability to type coroutines that use the ``async/await``
828-
syntax introduced in Python 3.5. For more information regarding coroutines and
829-
this new syntax, see :pep:`492`.
827+
Mypy lets you type coroutines that use the ``async/await`` syntax.
828+
For more information regarding coroutines, see :pep:`492` and the
829+
`asyncio documentation <https://docs.python.org/3/library/asyncio.html>`_.
830830

831-
Functions defined using ``async def`` are typed just like normal functions.
831+
Functions defined using ``async def`` are typed similar to normal functions.
832832
The return type annotation should be the same as the type of the value you
833833
expect to get back when ``await``-ing the coroutine.
834834

@@ -839,65 +839,40 @@ expect to get back when ``await``-ing the coroutine.
839839
async def format_string(tag: str, count: int) -> str:
840840
return f'T-minus {count} ({tag})'
841841
842-
async def countdown_1(tag: str, count: int) -> str:
842+
async def countdown(tag: str, count: int) -> str:
843843
while count > 0:
844-
my_str = await format_string(tag, count) # has type 'str'
844+
my_str = await format_string(tag, count) # type is inferred to be str
845845
print(my_str)
846846
await asyncio.sleep(0.1)
847847
count -= 1
848848
return "Blastoff!"
849849
850-
loop = asyncio.get_event_loop()
851-
loop.run_until_complete(countdown_1("Millennium Falcon", 5))
852-
loop.close()
850+
asyncio.run(countdown("Millennium Falcon", 5))
853851
854-
The result of calling an ``async def`` function *without awaiting* will be a
855-
value of type :py:class:`Coroutine[Any, Any, T] <typing.Coroutine>`, which is a subtype of
852+
The result of calling an ``async def`` function *without awaiting* will
853+
automatically be inferred to be a value of type
854+
:py:class:`Coroutine[Any, Any, T] <typing.Coroutine>`, which is a subtype of
856855
:py:class:`Awaitable[T] <typing.Awaitable>`:
857856

858857
.. code-block:: python
859858
860-
my_coroutine = countdown_1("Millennium Falcon", 5)
861-
reveal_type(my_coroutine) # has type 'Coroutine[Any, Any, str]'
859+
my_coroutine = countdown("Millennium Falcon", 5)
860+
reveal_type(my_coroutine) # Revealed type is "typing.Coroutine[Any, Any, builtins.str]"
862861
863-
.. note::
864-
865-
:ref:`reveal_type() <reveal-type>` displays the inferred static type of
866-
an expression.
867-
868-
You may also choose to create a subclass of :py:class:`~typing.Awaitable` instead:
869-
870-
.. code-block:: python
871-
872-
from typing import Any, Awaitable, Generator
873-
import asyncio
862+
.. _async-iterators:
874863

875-
class MyAwaitable(Awaitable[str]):
876-
def __init__(self, tag: str, count: int) -> None:
877-
self.tag = tag
878-
self.count = count
864+
Asynchronous iterators
865+
----------------------
879866

880-
def __await__(self) -> Generator[Any, None, str]:
881-
for i in range(n, 0, -1):
882-
print(f'T-minus {i} ({tag})')
883-
yield from asyncio.sleep(0.1)
884-
return "Blastoff!"
885-
886-
def countdown_3(tag: str, count: int) -> Awaitable[str]:
887-
return MyAwaitable(tag, count)
888-
889-
loop = asyncio.get_event_loop()
890-
loop.run_until_complete(countdown_3("Heart of Gold", 5))
891-
loop.close()
892-
893-
To create an iterable coroutine, subclass :py:class:`~typing.AsyncIterator`:
867+
If you have an asynchronous iterator, you can use the
868+
:py:class:`~typing.AsyncIterator` type in your annotations:
894869

895870
.. code-block:: python
896871
897872
from typing import Optional, AsyncIterator
898873
import asyncio
899874
900-
class arange(AsyncIterator[int]):
875+
class arange:
901876
def __init__(self, start: int, stop: int, step: int) -> None:
902877
self.start = start
903878
self.stop = stop
@@ -914,35 +889,78 @@ To create an iterable coroutine, subclass :py:class:`~typing.AsyncIterator`:
914889
else:
915890
return self.count
916891
917-
async def countdown_4(tag: str, n: int) -> str:
918-
async for i in arange(n, 0, -1):
892+
async def run_countdown(tag: str, countdown: AsyncIterator[int]) -> str:
893+
async for i in countdown:
919894
print(f'T-minus {i} ({tag})')
920895
await asyncio.sleep(0.1)
921896
return "Blastoff!"
922897
923-
loop = asyncio.get_event_loop()
924-
loop.run_until_complete(countdown_4("Serenity", 5))
925-
loop.close()
898+
asyncio.run(run_countdown("Serenity", arange(5, 0, -1)))
926899
927-
If you use coroutines in legacy code that was originally written for
928-
Python 3.4, which did not support the ``async def`` syntax, you would
929-
instead use the :py:func:`@asyncio.coroutine <asyncio.coroutine>`
930-
decorator to convert a generator into a coroutine, and use a
931-
generator type as the return type:
900+
Async generators (introduced in :pep:`525`) are an easy way to create
901+
async iterators:
932902

933903
.. code-block:: python
934904
935-
from typing import Any, Generator
905+
from typing import AsyncGenerator, Optional
936906
import asyncio
937907
938-
@asyncio.coroutine
939-
def countdown_2(tag: str, count: int) -> Generator[Any, None, str]:
940-
while count > 0:
941-
print(f'T-minus {count} ({tag})')
942-
yield from asyncio.sleep(0.1)
943-
count -= 1
944-
return "Blastoff!"
908+
# Could also type this as returning AsyncIterator[int]
909+
async def arange(start: int, stop: int, step: int) -> AsyncGenerator[int, None]:
910+
current = start
911+
while (step > 0 and current < stop) or (step < 0 and current > stop):
912+
yield current
913+
current += step
914+
915+
asyncio.run(run_countdown("Battlestar Galactica", arange(5, 0, -1)))
916+
917+
One common confusion is that the presence of a ``yield`` statement in an
918+
``async def`` function has an effect on the type of the function:
919+
920+
.. code-block:: python
921+
922+
from typing import AsyncIterator
923+
924+
async def arange(stop: int) -> AsyncIterator[int]:
925+
# When called, arange gives you an async iterator
926+
# Equivalent to Callable[[int], AsyncIterator[int]]
927+
i = 0
928+
while i < stop:
929+
yield i
930+
i += 1
931+
932+
async def coroutine(stop: int) -> AsyncIterator[int]:
933+
# When called, coroutine gives you something you can await to get an async iterator
934+
# Equivalent to Callable[[int], Coroutine[Any, Any, AsyncIterator[int]]]
935+
return arange(stop)
936+
937+
async def main() -> None:
938+
reveal_type(arange(5)) # Revealed type is "typing.AsyncIterator[builtins.int]"
939+
reveal_type(coroutine(5)) # Revealed type is "typing.Coroutine[Any, Any, typing.AsyncIterator[builtins.int]]"
940+
941+
await arange(5) # Error: Incompatible types in "await" (actual type "AsyncIterator[int]", expected type "Awaitable[Any]")
942+
reveal_type(await coroutine(5)) # Revealed type is "typing.AsyncIterator[builtins.int]"
943+
944+
This can sometimes come up when trying to define base classes or Protocols:
945+
946+
.. code-block:: python
947+
948+
from typing import AsyncIterator, Protocol
949+
950+
class LauncherIncorrect(Protocol):
951+
# Because launch does not have yield, this has type
952+
# Callable[[], Coroutine[Any, Any, AsyncIterator[int]]]
953+
# instead of
954+
# Callable[[], AsyncIterator[int]]
955+
async def launch(self) -> AsyncIterator[int]:
956+
raise NotImplementedError
957+
958+
class LauncherCorrect(Protocol):
959+
def launch(self) -> AsyncIterator[int]:
960+
raise NotImplementedError
945961
946-
loop = asyncio.get_event_loop()
947-
loop.run_until_complete(countdown_2("USS Enterprise", 5))
948-
loop.close()
962+
class LauncherAlsoCorrect(Protocol):
963+
async def launch(self) -> AsyncIterator[int]:
964+
raise NotImplementedError
965+
if False:
966+
yield 0

0 commit comments

Comments
 (0)