How to use the await keyword in the Python REPL without asyncio.run()

The Python REPL (read-eval-print loop), which is Python's interactive interpreter, is a great way for quickly testing simple Python commands. I use it quite often as a powerful command-line calculator, but also for exploring new Python libraries.

Many interesting Python libraries use asynchronous I/O with asyncio. This means that, instead of just calling a function directly, you have to await a coroutine. However, if you try this in the REPL, you encounter the following error message:

$ python
Python 3.10.6 (main, May 29 2023, 11:10:38) [GCC 11.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import asyncio
>>> await asyncio.sleep(5)
  File "<stdin>", line 1
SyntaxError: 'await' outside function
>>>

This is expected, because this line of code would never run in a Python script either. You would need to define a top-level coroutine with async def that calls one or more coroutines with await, and run the top-level coroutine with asyncio.run(). The canonical "Hello world" example from the Python documentation of coroutines looks something like this:

$ python
Python 3.10.6 (main, May 29 2023, 11:10:38) [GCC 11.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import asyncio
>>> async def main():
...     print("Hello")
...     await asyncio.sleep(5)
...     print("world")
...
>>> asyncio.run(main())
Hello
world
>>>

There's a five-second delay between the output of "Hello" and "world".

This seems a bit cumbersome. If you just want to call the asyncio.sleep() coroutine, you can simplify this to:

$ python
Python 3.10.6 (main, May 29 2023, 11:10:38) [GCC 11.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import asyncio
>>> asyncio.run(asyncio.sleep(5))
>>>

This is already better! However, it still means that every time you want to call a coroutine, you need to remember to wrap it inside an asyncio.run() call.

Fortunately, Python 3.8 introduced a top-level await if you run the asyncio module as a script:

$ python -m asyncio
asyncio REPL 3.10.6 (main, May 29 2023, 11:10:38) [GCC 11.3.0] on linux
Use "await" directly instead of "asyncio.run()".
Type "help", "copyright", "credits" or "license" for more information.
>>> import asyncio
>>> await asyncio.sleep(5)
>>>

The REPL helpfully mentions Use "await" directly instead of "asyncio.run()". before importing the asyncio module. [1] Then you can simply type await asyncio.sleep(5) without having to call asyncio.run().

Although this might not seem like much of an improvement in this particular case, when using asynchronous libraries in a Python REPL, having to add asyncio.run() for every coroutine call quickly becomes tedious.

For example, I can now easily request the Bluetooth adapters on my system (using Home Assistant's bluetooth-adapters package):

$ python -m asyncio
asyncio REPL 3.10.6 (main, May 29 2023, 11:10:38) [GCC 11.3.0] on linux
Use "await" directly instead of "asyncio.run()".
Type "help", "copyright", "credits" or "license" for more information.
>>> import asyncio
>>> from bluetooth_adapters import get_adapters
>>> adapters = get_adapters()
>>> await adapters.refresh()
>>> adapters.adapters
{'hci0': {'address': '9C:FC:E8:XX:XX:XX', 'sw_version': 'tux', 'hw_version': 'usb:v1D6Bp0246d0540', 'passive_scan': True, 'manufacturer': 'Intel Corporate', 'product': '0029', 'vendor_id': '8087', 'product_id': '0029'}}

Or, I can conduct a quick scan for Bluetooth Low Energy (BLE) devices in the vicinity (using the Bleak package):

$ python -m asyncio
asyncio REPL 3.10.6 (main, May 29 2023, 11:10:38) [GCC 11.3.0] on linux
Use "await" directly instead of "asyncio.run()".
Type "help", "copyright", "credits" or "license" for more information.
>>> import asyncio
>>> from bleak import BleakScanner
>>> devices = await BleakScanner.discover()
>>> [device.name for device in devices]
['Qingping Alarm Clock', 'abeacon_AC7D', 'TP358 (52C6)', '1B-0F-09-4F-89-F3', 'ThermoBeacon', 'F9-DA-D2-0D-62-24', '6A-C8-79-F4-E1-E5', 'Qingping BT Clock Lite', 'LYWSD02', 'Ruuvi 7E0E', 'TY', 'TP393 (2A3D)', 'Flower care', '52-9E-F1-64-DB-DF']

Give this top-level await approach a try with some of your favorite asynchronous Python libraries. You'll definitely become more productive in the REPL.