Вызов функции, объявленной через async def, не выполняет её тело, а возвращает объект-корутину. Выполнить его можно через await внутри другой корутины или запустив цикл событий: asyncio.run(main()).
Цикл событий (event loop) держит список задач. Корутина выполняется до ближайшего await на операции, которая ещё не готова (ответ сети, таймер). Там она приостанавливается, цикл запускает другую готовую задачу, а когда операция завершается — продолжает первую с того же места. Переключение кооперативное: корутина сама решает, где уступить, и только в await.
Одновременность. Последовательные await a(); await b() идут друг за другом. Чтобы задачи шли параллельно, их оборачивают в задачи: asyncio.gather(a(), b()), asyncio.create_task(...) или TaskGroup — последний правильно отменяет остальные задачи при ошибке.
Ограничения. await можно писать только внутри async def. Всё блокирующее внутри корутины останавливает цикл целиком: вместо time.sleep — asyncio.sleep, вместо requests — aiohttp или httpx. Блокирующий код, без которого не обойтись, выносят в поток: await asyncio.to_thread(func).
asyncio не ускоряет вычисления — это способ эффективно ждать много операций ввода-вывода.