Coroutine never awaited: главная ошибка новичка
Программа завершилась успешно и ничего не сделала: классика потерянного await. Разбираем корутин-призраков, честно смотрим на RuntimeWarning и выучиваем три лекарства.
Редакция Питоники
У этой ошибки всегда один сценарий. Скрипт написан, запускается без единой ошибки, завершается с гордым кодом выхода ноль — а результата нет: файл не сохранён, ответ не получен, печать внутри функции не сработала. Разработчик перечитывает код — всё «правильно»: функция объявлена, вызов на месте. На самом деле потерян один короткий символ — await перед вызовом. Эта ошибка настолько всеобщая, что её симптомы, диагноз и лечение собраны в отдельный урок.
Типичное место рождения этой ошибки — рефакторинг. Вчера функция была обычной: load() вызывался и возвращал результат. Сегодня она стала корутиной — и все старые вызовы молча превратились в фабрику корутин-призраков: синтаксис тот же, переменные те же, а действия нет. Поэтому async-переписывание всегда заканчивается обходом мест вызова — поиск по имени функции сэкономит вечер отладки.
Как это выглядит: корутина-призрак
Воспроизведём ситуацию и проведём вскрытие. Внутри тела корутины стоит флажок: он поднимется, только если тело хотя бы началось. Вызовем корутину без await и спросим у inspect.iscoroutine, что лежит в переменной.
import asyncio
import inspect
started = False
async def fetch():
global started
started = True
return "готово"
coro = fetch() # await потерялся
print("Это корутина:", inspect.iscoroutine(coro))
print("Тело выполнилось:", started)
coro.close() # закрыли объект без запуска
Это корутина: True Тело выполнилось: False
Диагноз на экране: в coro лежит настоящий объект корутины, а тело не выполнилось ни на строчку — флажок started остался False. Вызов async-функции — это не «сделать», а «подготовить к выполнению»: Python собрал кадр с кодом и аргументами и отдал его тебе в руки. Дальше кадр должен кто-то раскрутить — await, asyncio.run или задача. Никто не раскрутил — кадр остался запечатанным, и скрипт закончился, «успешно» не сделав ничего.
import asyncio
async def fetch():
await asyncio.sleep(1)
return "готово"
fetch() # await потерялся
print("скрипт завершён")
RuntimeWarning: coroutine 'fetch' was never awaited скрипт завершён
Обрати внимание на порядок: предупреждение появилось раньше, чем скрипт напечатал свою строку, — корутина умерла в момент, когда ссылка на неё потерялась (в CPython объекты уничтожаются сразу, как только падает счётчик ссылок). И ещё: предупреждение не остановило программу. Именно поэтому ошибка такая живучая — она не ломает запуск, а просто глушит результат.
Три лекарства
Запускать корутину умеют ровно три вещи, и выбор между ними — про то, «ждать ли результат сейчас».
- await — внутри другой корутины: выполнить сейчас и дождаться результата;
- asyncio.run — снаружи, из синхронного мира: стать точкой входа (урок 3);
- asyncio.create_task — внутри цикла: запустить сейчас, а дождаться позже (урок 6).
import asyncio
started = False
async def fetch():
global started
started = True
return "готово"
async def main():
result = await fetch()
print("Тело выполнилось:", started)
print("Результат:", result)
asyncio.run(main())
Тело выполнилось: True Результат: готово
import asyncio
async def fetch():
await asyncio.sleep(0)
return "готово"
print("Результат:", asyncio.run(fetch()))
Результат: готово
Третье лекарство — asyncio.create_task — заслуживает отдельного урока: он запускает корутину сейчас, не дожидаясь её в этой строке, и это способ запуска «в фоне». Пока запомни сам факт: нераспечатанный результат, «пропавшая» работа и never awaited в терминале лечатся одним из трёх способов, и все три сводятся к одному — корутину должен кто-то запустить.
Как выбрать из трёх лекарств, подскажет вопрос «что мне нужно прямо сейчас». Жду результат в этой строке — await. Корутина — старт всей программы — asyncio.run в самом низу файла. Работу нужно запустить и пойти дальше, а догоню позже — create_task, и это главный герой урока 6. В сомнительных случаях работает страховка из раздела ниже: проверь iscoroutine и дозапусти.
Вариации на ту же ошибку
Потерянный await редко живёт один: у него есть семейка симптомов, которые ломают программу уже по-другому. Первый — результат присвоили, но не запустили: в переменной лежит объект корутины, и первая же строковая операция падает с AttributeError.
result = fetch() # корутина, а не строка
print(result.upper())
AttributeError: 'coroutine' object has no attribute 'upper'
Слово coroutine в сообщении об ошибке — подарок: увидел AttributeError: 'coroutine' object has no attribute ... — иди выше по коду и ищи вызов без await. Вторая вариация — функция-обёртка, которая возвращает корутину вместо результата: вызывающий уверен, что получил готовое значение.
import asyncio
async def fetch(name):
await asyncio.sleep(0)
return f"{name}: готово"
def load_page(): # обычная функция...
return fetch("Каталог") # ...но вернула корутину
page = load_page()
print("Тип:", type(page).__name__) # не строка!
Тип: coroutine
Здесь ошибки нет вовсе — просто page оказалось не тем, чем казалось: type(page).__name__ честно напечатал coroutine. В большом проекте такие места находят по одному правилу: каждый вызов async-функции либо ждут (await), либо явно запускают (asyncio.run, create_task) — «просто вызвать» не бывает.
Как ловить корутин-призраков
Первый инструмент — уже знакомая проверка inspect.iscoroutine: она отличает не запущенный объект от результата и отлично работает страховкой в спорных местах — обёртках, кэшах, колбэках, где корутина может прийти «транзитом».
import asyncio
import inspect
async def fetch():
await asyncio.sleep(0)
return "готово"
async def main():
outcome = fetch() # представим: корутина пришла извне
print("Прилетела корутина:", inspect.iscoroutine(outcome))
if inspect.iscoroutine(outcome):
outcome = await outcome # дозапустили - предохранитель
print("Результат:", outcome)
asyncio.run(main())
Прилетела корутина: True Результат: готово
Второй инструмент — редактор и линтеры: современные IDE подчёркивают вызовы корутин, чей результат никак не использован, а линтеры (ruff, pylint) умеют находить и брошенные корутины, и повисшие задачи. Включённые подсказки ловят ошибку до запуска — дешевле любого отладчика. Третий — честное чтение stderr: дома запускай скрипты из терминала и дочитывай вывод до конца, RuntimeWarning в хвосте — это всегда место, где результат утёк.
| Симптом | Причина | Лекарство |
|---|---|---|
| Программа завершилась молча, результата нет | вызов без await: корутина-призрак | await перед вызовом |
| RuntimeWarning: ... never awaited в терминале | брошенный объект корутины уничтожен | тот же await — или create_task для фона |
| AttributeError: 'coroutine' object has no attribute | в переменной корутина вместо результата | await при присваивании |
| type(x) показывает coroutine | обёртка вернула корутину | await на границе обёртки |
Таблица пригодится и как чек-лист код-ревью: встретил в чужом пулреквесте вызов async-функции — пробеги глазами четыре симптома. Они покрывают практически все проявления потерянного await, а лечение всегда одно: кто-то должен дождаться корутину — программа, строка или задача.
import asyncio
import time
START = time.monotonic()
async def fetch(name, delay):
await asyncio.sleep(delay) # имитация сетевой задержки
t = round(time.monotonic() - START)
return f"{name}: готово на t={t}"
async def main():
first = await fetch("Главная", 1)
second = await fetch("Каталог", 1)
print(first)
print(second)
asyncio.run(main())
Главная: готово на t=1 Каталог: готово на t=2
Прогони глазами эту программу ещё раз: ни одного вызова без await, каждая корутина дожидается, каждая печатает по факту. Ничего лишнего — и именно так выглядит код, в котором never awaited не заводится: не запретами, а привычкой дописывать await сразу за вызовом.
Что дальше
Теперь главный грех asyncio вскрывается с поличным: вызов без await — заготовка без запуска, RuntimeWarning в stderr вместо ошибки, три лекарства на выбор и inspect.iscoroutine как карманный детектив. Дальше логика курса ведёт к главному вопросу: сколько стоят два await подряд — урок с секундомером, который покажет, почему последовательные await не ускоряются, и проложит мост к задачам. А запускать корутины «в фоне» научит урок 6 про create_task.
Программа завершилась успешно и ничего не сделала: так выглядит забытый await — корутина создана, но ни разу не запущена.
Сначала предскажи ответ в голове — это главный навык программиста.
import asyncio
done = False
async def work():
global done
done = True
return "ок"
async def main():
result = work()
print("done =", done)
asyncio.run(main())
import asyncio
import inspect
async def fetch():
await asyncio.sleep(0)
return "готово"
async def main():
a = fetch()
b = await a
print(inspect.iscoroutine(a), b)
asyncio.run(main())
import asyncio
async def shout():
await asyncio.sleep(0)
return "ау"
async def main():
outcome = await shout()
print(outcome * 2)
asyncio.run(main())
1. Что происходит с корутиной, вызванной без await и брошенной?
2. Почему в браузерной песочнице never awaited незаметен?
3. Какое сообщение выдаст result.upper(), если result = fetch() без await?
4. Какие три способа запустить корутину существуют?
5. Что вернёт inspect.iscoroutine(x) после строки x = await fetch()?
6. Почему Python не поднимает ошибку при брошенной корутине?
Почини программу: в main корутина prepare вызвана без await, и «результат» печатает тип вместо содержимого. Добавь проверку inspect.iscoroutine, дозапусти корутину await'ом и добейся вывода двух строк: «Это корутина: True» и «Результат: отчёт готов».
Что означает RuntimeWarning: coroutine ... was never awaited?
Ты вызвал async-функцию без await: создался объект корутины, тело не выполнилось, и при уничтожении объекта Python предупредил «never awaited». Программа при этом завершается без ошибки, просто результата нет. Лечение — await перед вызовом, asyncio.run снаружи или asyncio.create_task для фоновой работы.
Почему моя async-функция не выполняется, хотя ошибок нет?
Это тот же потерянный await. Проверь место вызова: fetch(...) создаёт объект корутины, await fetch(...) — выполняет. Карманный тест: import inspect и inspect.iscoroutine(x) — если для переменной, которая должна быть результатом, он вернул True, где-то выше await потерялся.
Как найти забытые await в большом проекте?
Три пути: включить подсказки линтера (ruff и pylint умеют подсвечивать неиспользованные корутины и повисшие задачи), читать stderr при запуске из терминала — RuntimeWarning печатается в самом конце вывода, — и проверять результаты по факту: что должно было быть напечатано или сохранено. Ошибка не роняет программу, поэтому искать её надо не по крашу, а по отсутствию эффекта.
Опасно ли игнорировать RuntimeWarning, если всё «и так работает»?
Да, опасно: never awaited означает, что какая-то работа реально не сделана — данные не скачаны, запись не записана. Тесты на «не упало» такое пропускают, а последствия приезжают позже. Предупреждение стоит считать багом и править await'ом, а не глушить фильтрами warnings.
Понравился урок? Сошлитесь на него
«Программа завершилась успешно и ничего не сделала: так выглядит забытый await — корутина создана, но ни разу не запущена.»
Скопируйте готовую ссылку в формате HTML, Markdown или чистый адрес и вставьте в статью на Habr, VC, Telegram-канал или свой блог — так о проекте узнают новые читатели.
Что читать дальше
asyncio · Урок 2
Первая корутина: async def и await
async def — функция с паузами, await — «дождись результата». Звучит просто, пока не выясняется, что вызов async-функции не выполняет её тело. Разбираем оба ключевых слова на живом коде.
asyncio · Урок 5
Два await подряд: последовательность, не конкурентность
Поставил два await — и ожидаешь ускорения? Секундомер говорит: 2 + 2 = 4 секунды. Разбираем, почему await сам по себе не создаёт конкурентность, и замеряем всё честно.
asyncio · Урок 6
Задачи: asyncio.create_task
Одна строка — и два await из прошлого урока бегут одновременно: 4 секунды становятся 2. Знакомимся с задачами, их done() и result() и правилом о ссылке на задачу.
Похожие уроки по темам
Подобраны автоматически по пересечению тем и ключевых слов.
aiogram · Урок 7
Асинхронность в aiogram: asyncio для бота без страха
Разбираем на живом коде, зачем aiogram асинхронный: async и await, asyncio.sleep против time.sleep и gather для параллельных чатов.
asyncio для начинающихaiogram async
pytest · Урок 9
Проверка исключений: pytest.raises
Хорошая функция на плохом входе не молчит, а бросает исключение — и тест обязан это проверять. pytest.raises делает это одной строкой внутри with.
как тестировать ошибки python
asyncio · Урок 19
Читаем async-ошибки: словарь типовых сообщений
Сообщения asyncio пугают, пока их не переведёшь. Четыре типовые ошибки — причина, лечение, живой пример на каждую.
asyncio ошибки python разборasyncio ошибки