Skip to content

Commit 9917b46

Browse files
committed
Improve async documentation
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 python#5385 since it comes up not infrequently. Linking python#13681
1 parent dfe0281 commit 9917b46

File tree

1 file changed

+82
-64
lines changed

1 file changed

+82
-64
lines changed

docs/source/more_types.rst

Lines changed: 82 additions & 64 deletions
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)