Перейти к содержанию

192 уроков, 14 библиотек и челлендж «Что выведет код?» — бесплатно, код прямо в браузере

Начать обучение
Урок 4 из 20 Начальный 30 мин 120 XP

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
скрипт завершён
Блок не запускается в песочнице: предупреждение печатается в stderr, который песочница не показывает. Вывод сокращён до сути: предупреждение возникает при уничтожении корутины, строка скрипта — после; в терминале будут ещё служебные строки с именем файла и tracemalloc.

Обрати внимание на порядок: предупреждение появилось раньше, чем скрипт напечатал свою строку, — корутина умерла в момент, когда ссылка на неё потерялась (в CPython объекты уничтожаются сразу, как только падает счётчик ссылок). И ещё: предупреждение не остановило программу. Именно поэтому ошибка такая живучая — она не ломает запуск, а просто глушит результат.

Три лекарства

Запускать корутину умеют ровно три вещи, и выбор между ними — про то, «ждать ли результат сейчас».

  • await — внутри другой корутины: выполнить сейчас и дождаться результата;
  • asyncio.run — снаружи, из синхронного мира: стать точкой входа (урок 3);
  • asyncio.create_task — внутри цикла: запустить сейчас, а дождаться позже (урок 6).
лекарство 1: await
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
Результат: готово
лекарство 2: asyncio.run
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.upper() на объекте корутины
result = fetch()          # корутина, а не строка
print(result.upper())
Вывод
AttributeError: 'coroutine' object has no attribute 'upper'
Блок не запускается намеренно: он вызывает ошибку. Слова 'coroutine' object в сообщении — прямая подсказка: результат не await'нули.

Слово 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())
Проверь себя
0 / 6

1. Что происходит с корутиной, вызванной без await и брошенной?

2. Почему в браузерной песочнице never awaited незаметен?

3. Какое сообщение выдаст result.upper(), если result = fetch() без await?

4. Какие три способа запустить корутину существуют?

5. Что вернёт inspect.iscoroutine(x) после строки x = await fetch()?

6. Почему Python не поднимает ошибку при брошенной корутине?

Карточки терминов
Запомнено: 0 / 6
Практика

Почини программу: в main корутина prepare вызвана без await, и «результат» печатает тип вместо содержимого. Добавь проверку inspect.iscoroutine, дозапусти корутину await'ом и добейся вывода двух строк: «Это корутина: True» и «Результат: отчёт готов».

practice.py
Вопросы и ответы по уроку

Что означает 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-канал или свой блог — так о проекте узнают новые читатели.

TelegramVK

Похожие уроки по темам

Подобраны автоматически по пересечению тем и ключевых слов.