{ "cells": [ { "cell_type": "markdown", "id": "9a8f2f85", "metadata": {}, "source": [ "# `asyncio` example\n", "\n", "From IPython≥7.0 you can use `asyncio` directly in Jupyter Notebooks, see also [IPython 7.0, Async REPL](https://blog.jupyter.org/ipython-7-0-async-repl-a35ce050f7f7).\n", "\n" ] }, { "cell_type": "markdown", "id": "a728279f", "metadata": {}, "source": [ "If you get `RuntimeError: This event loop is already running`, [nest-asyncio] might help you.\n", "\n", "You can install the package with\n", "\n", "``` bash\n", "$ uv add nest-asyncio\n", "```\n", "\n", "You can then import it into your notebook and use it with:" ] }, { "cell_type": "code", "execution_count": 1, "id": "45aacd96", "metadata": { "execution": { "iopub.execute_input": "2026-05-19T18:53:26.010838Z", "iopub.status.busy": "2026-05-19T18:53:26.010607Z", "iopub.status.idle": "2026-05-19T18:53:26.016462Z", "shell.execute_reply": "2026-05-19T18:53:26.016080Z", "shell.execute_reply.started": "2026-05-19T18:53:26.010820Z" } }, "outputs": [], "source": [ "import nest_asyncio\n", "\n", "\n", "nest_asyncio.apply()" ] }, { "cell_type": "markdown", "id": "109900af", "metadata": {}, "source": [ "
\n", "\n", "**See also:**\n", "\n", "* [asyncio: We Did It Wrong](https://www.roguelynn.com/words/asyncio-we-did-it-wrong/) by Lynn Root\n", "* [An Intro to asyncio](https://blog.pythonlibrary.org/2016/07/26/python-3-an-intro-to-asyncio/) by Mike Driscoll\n", "* [Asyncio Coroutine Patterns: Beyond await](https://medium.com/python-pandemonium/asyncio-coroutine-patterns-beyond-await-a6121486656f) by Yeray Diaz\n", "
" ] }, { "cell_type": "markdown", "id": "bc3e0b72", "metadata": {}, "source": [ "## Simple *Hello world* example" ] }, { "cell_type": "code", "execution_count": 2, "id": "479b2752", "metadata": { "execution": { "iopub.execute_input": "2026-05-19T18:53:26.017283Z", "iopub.status.busy": "2026-05-19T18:53:26.017090Z", "iopub.status.idle": "2026-05-19T18:53:27.024876Z", "shell.execute_reply": "2026-05-19T18:53:27.023788Z", "shell.execute_reply.started": "2026-05-19T18:53:26.017264Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Hello\n", "world\n" ] } ], "source": [ "import asyncio\n", "\n", "\n", "async def hello():\n", " print(\"Hello\")\n", " await asyncio.sleep(1)\n", " print(\"world\")\n", "\n", "\n", "await hello()" ] }, { "cell_type": "markdown", "id": "c3d8764e", "metadata": {}, "source": [ "## A little bit closer to a real world example" ] }, { "cell_type": "code", "execution_count": 3, "id": "fc1761e7", "metadata": { "execution": { "iopub.execute_input": "2026-05-19T18:53:27.028650Z", "iopub.status.busy": "2026-05-19T18:53:27.028103Z", "iopub.status.idle": "2026-05-19T18:53:34.114538Z", "shell.execute_reply": "2026-05-19T18:53:34.113994Z", "shell.execute_reply.started": "2026-05-19T18:53:27.028619Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Publishing 1/10\n", "Publishing 2/10\n", "consuming 1\n", "Publishing 3/10\n", "consuming 2\n", "Publishing 4/10\n", "consuming 3\n", "Publishing 5/10\n", "consuming 4\n", "Publishing 6/10\n", "consuming 5\n", "Publishing 7/10\n", "consuming 6\n", "Publishing 8/10\n", "Publishing 9/10\n", "consuming 7\n", "Publishing 10/10\n", "consuming 8\n", "consuming 9\n", "consuming 10\n" ] } ], "source": [ "import random\n", "\n", "\n", "async def publish(queue, n):\n", " for x in range(1, n + 1):\n", " # publish an item\n", " print(f\"Publishing {x}/{n}\")\n", " # simulate i/o operation using sleep\n", " await asyncio.sleep(random.random())\n", " item = str(x)\n", " # put the item in the queue\n", " await queue.put(item)\n", "\n", " # indicate the publisher is done\n", " await queue.put(None)\n", "\n", "\n", "async def consume(queue):\n", " while True:\n", " # wait for an item from the publisher\n", " item = await queue.get()\n", " if item is None:\n", " # the publisher emits None to indicate that it is done\n", " break\n", "\n", " # process the item\n", " print(f\"consuming {item}\")\n", " # simulate i/o operation using sleep\n", " await asyncio.sleep(random.random())\n", "\n", "\n", "background_tasks = set()\n", "loop = asyncio.get_event_loop()\n", "queue = asyncio.Queue()\n", "publishing = asyncio.ensure_future(publish(queue, 10), loop=loop)\n", "background_tasks.add(publishing)\n", "publishing.add_done_callback(background_tasks.discard)\n", "loop.run_until_complete(consume(queue))" ] }, { "cell_type": "markdown", "id": "80a9947d", "metadata": {}, "source": [ "## Exception Handling\n", "\n", "
\n", "\n", "**See also:**\n", "\n", "* [set_exception_handler](https://docs.python.org/3/library/asyncio-eventloop.html#asyncio.loop.set_exception_handler)\n", "
" ] }, { "cell_type": "code", "execution_count": 4, "id": "54d7107d", "metadata": { "execution": { "iopub.execute_input": "2026-05-19T18:53:34.115618Z", "iopub.status.busy": "2026-05-19T18:53:34.115410Z", "iopub.status.idle": "2026-05-19T18:53:34.120519Z", "shell.execute_reply": "2026-05-19T18:53:34.119936Z", "shell.execute_reply.started": "2026-05-19T18:53:34.115596Z" } }, "outputs": [], "source": [ "import logging\n", "import signal\n", "\n", "\n", "logger = logging.getLogger(\"stream_logger\")\n", "\n", "\n", "def handle_exception(context):\n", " msg = context.get(\"Exception\", context[\"message\"])\n", " logger.error(f\"Caught exception: {msg}\")\n", " logger.info(\"Shutting down…\")\n", "\n", "\n", "def main():\n", " loop = asyncio.get_event_loop()\n", " # May want to catch other signals too\n", " signals = (signal.SIGHUP, signal.SIGTERM, signal.SIGINT)\n", " for s in signals:\n", " loop.add_signal_handler(\n", " s,\n", " lambda s=s: asyncio.create_task(loop, signal=s),\n", " )\n", " loop.set_exception_handler(handle_exception)\n", " queue = asyncio.Queue()\n", " try:\n", " publish_task = loop.create_task(publish(queue))\n", " background_tasks.add(publish_task)\n", " publish_task.add_done_callback(background_tasks.discard)\n", " consume_task = loop.create_task(consume(queue))\n", " background_tasks.add(consume_task)\n", " consume_task.add_done_callback(background_tasks.discard)\n", " loop.run_forever()\n", " finally:\n", " loop.close()" ] }, { "cell_type": "markdown", "id": "8b4270b5", "metadata": {}, "source": [ "## Testing with `pytest`" ] }, { "cell_type": "markdown", "id": "47a0aa94", "metadata": {}, "source": [ "### Example:\n", "\n", "When testing, you often need to simulate coroutines that are called within the function you are testing.\n", "\n", "For this, we need\n", "* [pytest](https://docs.pytest.org/en/stable/)\n", "* [pytest-asyncio](https://pytest-asyncio.readthedocs.io/en/stable/)\n", "* [pytest-mock](https://pytest-mock.readthedocs.io/en/latest/)\n", "\n", "However, the pytest-mock library does not support asynchronous mocks, so we need to find a workaround:" ] }, { "cell_type": "code", "execution_count": 5, "id": "62ce20d0-c095-48d7-ad59-388efaf78448", "metadata": { "execution": { "iopub.execute_input": "2026-05-19T18:53:34.121319Z", "iopub.status.busy": "2026-05-19T18:53:34.121170Z", "iopub.status.idle": "2026-05-19T18:53:34.173862Z", "shell.execute_reply": "2026-05-19T18:53:34.173617Z", "shell.execute_reply.started": "2026-05-19T18:53:34.121305Z" } }, "outputs": [], "source": [ "import asyncio\n", "\n", "import pytest\n", "\n", "\n", "@pytest.fixture\n", "def mock_coroutine(mocker, monkeypatch):\n", "\n", " def _mock_coroutine_pair(to_patch=None):\n", " mock = mocker.Mock()\n", "\n", " async def coroutine(*args, **kwargs):\n", " return mock(*args, **kwargs)\n", "\n", " if to_patch:\n", " monkeypatch.setattr(to_patch, _mock_coroutine_pair)\n", "\n", " return mock, _mock_coroutine_pair\n", "\n", " return _mock_coroutine_pair" ] }, { "cell_type": "markdown", "id": "841d54fd-9cda-4076-b3ed-3b349ca3accf", "metadata": { "execution": { "iopub.execute_input": "2026-05-19T16:21:45.622232Z", "iopub.status.busy": "2026-05-19T16:21:45.621851Z", "iopub.status.idle": "2026-05-19T16:21:45.629754Z", "shell.execute_reply": "2026-05-19T16:21:45.629205Z", "shell.execute_reply.started": "2026-05-19T16:21:45.622203Z" } }, "source": [ "And for [asyncio.Queue](https://docs.python.org/3/library/asyncio-queue.html#queue), I have the following fixtures:" ] }, { "cell_type": "code", "execution_count": 6, "id": "9fe17e42-eb2a-43fb-b5ac-333a99fb1c81", "metadata": { "execution": { "iopub.execute_input": "2026-05-19T18:53:34.174524Z", "iopub.status.busy": "2026-05-19T18:53:34.174379Z", "iopub.status.idle": "2026-05-19T18:53:34.176464Z", "shell.execute_reply": "2026-05-19T18:53:34.176123Z", "shell.execute_reply.started": "2026-05-19T18:53:34.174509Z" } }, "outputs": [], "source": [ "@pytest.fixture\n", "def mock_queue(mocker, monkeypatch):\n", " queue = mocker.Mock()\n", " monkeypatch.setattr(asyncio, \"Queue\", queue)\n", " return queue.return_value\n", "\n", "\n", "@pytest.fixture\n", "def mock_get(mock_coroutine):\n", " mock_get, _ = mock_coroutine()\n", " return mock_get" ] }, { "cell_type": "code", "execution_count": 7, "id": "a196e119", "metadata": { "execution": { "iopub.execute_input": "2026-05-19T18:53:34.176891Z", "iopub.status.busy": "2026-05-19T18:53:34.176808Z", "iopub.status.idle": "2026-05-19T18:53:34.178631Z", "shell.execute_reply": "2026-05-19T18:53:34.178381Z", "shell.execute_reply.started": "2026-05-19T18:53:34.176884Z" } }, "outputs": [], "source": [ "@pytest.mark.asyncio\n", "async def test_consume(mock_get, mock_queue):\n", " mock_get.side_effect = Exception(\"Break while loop\")\n", "\n", " with pytest.raises(Exception, match=\"Break while loop\"):\n", " await consume(mock_queue)" ] }, { "cell_type": "markdown", "id": "1ea35d70", "metadata": {}, "source": [ "### Third-party libraries\n", "\n", "* [pytest-asyncio](https://github.com/pytest-dev/pytest-asyncio) has helpful things like fixtures for `event_loop`, `unused_tcp_port`, and `unused_tcp_port_factory`; and the ability to create your own [asynchronous fixtures](https://pytest-asyncio.readthedocs.io/en/latest/reference/fixtures/index.html).\n", "* [asynctest](https://asynctest.readthedocs.io/en/latest/index.html) has helpful tooling, including coroutine mocks and [exhaust_callbacks](https://asynctest.readthedocs.io/en/latest/asynctest.helpers.html#asynctest.helpers.exhaust_callbacks) so we don’t have to manually await tasks.\n", "* [aiohttp](https://docs.aiohttp.org/en/stable/) has some really nice built-in test utilities." ] }, { "cell_type": "markdown", "id": "8f9acdb7", "metadata": {}, "source": [ "## Debugging\n", "\n", "`asyncio` already has a [debug mode](https://docs.python.org/3.6/library/asyncio-dev.html#debug-mode-of-asyncio) in the standard library. You can simply activate it with the `PYTHONASYNCIODEBUG` environment variable or in the code with `loop.set_debug(True)`." ] }, { "cell_type": "markdown", "id": "6ef4d1c5", "metadata": {}, "source": [ "### Using the debug mode to identify slow async calls\n", "\n", "`asyncio`’s debug mode has a tiny built-in profiler. When debug mode is on, `asyncio` will log any asynchronous calls that take longer than 100 milliseconds." ] }, { "cell_type": "markdown", "id": "f4daa532", "metadata": {}, "source": [ "### Debugging in production with `aiodebug`\n", "\n", "[aiodebug](https://gitlab.com/quantlane/libs/aiodebug) is a tiny library for monitoring and testing asyncio programs." ] }, { "cell_type": "markdown", "id": "584b01ce", "metadata": {}, "source": [ "#### Example" ] }, { "cell_type": "code", "execution_count": 8, "id": "1da72b73-bc81-4f42-898c-b8428613f085", "metadata": { "execution": { "iopub.execute_input": "2026-05-19T18:53:34.179218Z", "iopub.status.busy": "2026-05-19T18:53:34.179140Z", "iopub.status.idle": "2026-05-19T18:53:34.182810Z", "shell.execute_reply": "2026-05-19T18:53:34.182415Z", "shell.execute_reply.started": "2026-05-19T18:53:34.179211Z" } }, "outputs": [], "source": [ "import aiodebug.log_slow_callbacks\n", "\n", "from aiodebug.logging_compat import get_logger\n", "\n", "\n", "logger = get_logger(__name__)\n", "\n", "aiodebug.log_slow_callbacks.enable(\n", " 0.05,\n", " on_slow_callback=lambda task_name, duration: logger.warning(\n", " \"Task blocked async loop for too long\",\n", " extra={\"task_name\": task_name, \"duration\": duration},\n", " ),\n", ")" ] }, { "cell_type": "markdown", "id": "dac368ac", "metadata": {}, "source": [ "## Logging\n", "\n", "[aiologger](https://github.com/async-worker/aiologger) allows non-blocking logging." ] }, { "cell_type": "markdown", "id": "050af8ac", "metadata": {}, "source": [ "## Asynchronous Widgets\n", "\n", "
\n", "\n", "**See also:**\n", "\n", "* [Asynchronous Widgets](https://ipywidgets.readthedocs.io/en/stable/examples/Widget%20Asynchronous.html)\n", "
" ] }, { "cell_type": "code", "execution_count": 9, "id": "6925cd9d", "metadata": { "execution": { "iopub.execute_input": "2026-05-19T18:53:34.183314Z", "iopub.status.busy": "2026-05-19T18:53:34.183229Z", "iopub.status.idle": "2026-05-19T18:53:34.185135Z", "shell.execute_reply": "2026-05-19T18:53:34.184913Z", "shell.execute_reply.started": "2026-05-19T18:53:34.183306Z" } }, "outputs": [], "source": [ "def wait_for_change(widget, value):\n", " future = asyncio.Future()\n", "\n", " def getvalue(change):\n", " # make the new value available\n", " future.set_result(change.new)\n", " widget.unobserve(getvalue, value)\n", "\n", " widget.observe(getvalue, value)\n", " return future" ] }, { "cell_type": "code", "execution_count": 10, "id": "fb57d28a", "metadata": { "execution": { "iopub.execute_input": "2026-05-19T18:53:34.185590Z", "iopub.status.busy": "2026-05-19T18:53:34.185514Z", "iopub.status.idle": "2026-05-19T18:53:34.218839Z", "shell.execute_reply": "2026-05-19T18:53:34.218589Z", "shell.execute_reply.started": "2026-05-19T18:53:34.185583Z" } }, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "ba5c023dd05a4f64bc79a3488a517b38", "version_major": 2, "version_minor": 0 }, "text/plain": [ "IntSlider(value=0)" ] }, "execution_count": 10, "metadata": {}, "output_type": "execute_result" }, { "name": "stdout", "output_type": "stream", "text": [ "did work 0\n", "async function continued with value 1\n", "did work 1\n", "async function continued with value 3\n", "did work 2\n", "async function continued with value 7\n", "did work 3\n", "async function continued with value 12\n", "did work 4\n", "async function continued with value 21\n", "did work 5\n", "async function continued with value 31\n", "did work 6\n", "async function continued with value 43\n", "did work 7\n", "async function continued with value 55\n", "did work 8\n", "async function continued with value 70\n", "did work 9\n", "async function continued with value 87\n" ] } ], "source": [ "from ipywidgets import IntSlider\n", "\n", "\n", "slider = IntSlider()\n", "\n", "\n", "async def f():\n", " for i in range(10):\n", " print(f\"did work {i}\")\n", " x = await wait_for_change(slider, \"value\")\n", " print(f\"async function continued with value {x}\")\n", "\n", "\n", "task = asyncio.ensure_future(f())\n", "background_tasks = set()\n", "\n", "background_tasks.add(task)\n", "\n", "task.add_done_callback(background_tasks.discard)\n", "\n", "slider" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3.13 Kernel", "language": "python", "name": "python313" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.13.0" }, "latex_envs": { "LaTeX_envs_menu_present": true, "autoclose": false, "autocomplete": true, "bibliofile": "biblio.bib", "cite_by": "apalike", "current_citInitial": 1, "eqLabelWithNumbers": true, "eqNumInitial": 1, "hotkeys": { "equation": "Ctrl-E", "itemize": "Ctrl-I" }, "labels_anchors": false, "latex_user_defs": false, "report_style_numbering": false, "user_envs_cfg": false }, "widgets": { "application/vnd.jupyter.widget-state+json": { "state": { "56cd9ec3872d483f98e2aac9cd7e6d79": { "model_module": "@jupyter-widgets/controls", "model_module_version": "2.0.0", "model_name": "SliderStyleModel", "state": { "description_width": "" } }, "ba5c023dd05a4f64bc79a3488a517b38": { "model_module": "@jupyter-widgets/controls", "model_module_version": "2.0.0", "model_name": "IntSliderModel", "state": { "behavior": "drag-tap", "layout": "IPY_MODEL_f787d864e3e84d42bf74f98725ce3fde", "style": "IPY_MODEL_56cd9ec3872d483f98e2aac9cd7e6d79", "value": 100 } }, "f787d864e3e84d42bf74f98725ce3fde": { "model_module": "@jupyter-widgets/base", "model_module_version": "2.0.0", "model_name": "LayoutModel", "state": {} } }, "version_major": 2, "version_minor": 0 } } }, "nbformat": 4, "nbformat_minor": 5 }