pytest 9.2: быстрее в CI, понятные ошибки плагинов и строгий parametrize

Обложка: pytest 9.2 и логотип Python

27 октября 2026 года команда pytest выпустила версию 9.2.0. Это минорный выпуск, но в нём есть изменения, из-за которых старый тестовый код может упасть с ошибкой там, где раньше молча работал. Плюс заметное ускорение в CI и несколько предупреждений о том, что уберут в pytest 10.

Для тех, кто гоняет тесты в GitLab CI или GitHub Actions, главное здесь кэш перезаписанных модулей и новые коды выхода при сломанных плагинах. Ставится обычно: pip install -U pytest или uv add --dev pytest. Минимальная версия Python осталась 3.10, хотя сама 3.10 уже без поддержки.

Что может сломаться после обновления

Аргументы indirect, ids и scope в @pytest.mark.parametrize теперь передаются только по имени. Раньше частая ошибка выглядела так: написали parametrize("arg1", "arg2", [(1, 2)]), список значений попадал в indirect, и pytest ругался на несуществующую фикстуру (1, 2). Теперь сразу будет понятный TypeError. В аннотациях типов эти аргументы и так были именованными, так что код, который проходил mypy, не пострадает.

@pytest.mark.parametrize("a,b", [(1, 2), (3, 4)], ids=["small", "big"])
def test_sum(a, b):
    assert a + b > 0

Ключ -c теперь падает с ошибкой использования, если путь к конфигу не существует, указывает на каталог или на файл неизвестного формата. Раньше такой путь либо игнорировался, либо вылетал необработанный FileNotFoundError.

Сломанные плагины больше не роняют запуск голым трейсбеком с кодом 1. Если плагин не найден через -p, pytest_plugins или PYTEST_PLUGINS, pytest завершится с кодом 4 (USAGE_ERROR), как при сломанном conftest.py. Если плагин найден, но падает при импорте, например из-за кривой точки входа pytest11 или недостающей зависимости, будет код 3 (INTERNAL_ERROR) с сохранённым трейсбеком. pytest.main() теперь возвращает эти коды, а не бросает ImportError. Если ваш CI различает коды выхода, это стоит учесть.

Что объявили устаревшим и уберут в pytest 10 или 11

  • Декоратор yield_fixture запланирован к удалению. Если он ещё где-то остался, меняйте на обычный @pytest.fixture с yield внутри.
  • Фикстуры с областью module, package и session, объявленные как методы экземпляра класса, теперь устарели. Для фикстур уровня класса это сделали ещё в 9.1.
  • Если в [tool.pytest.ini_options] в pyproject.toml строковой опции передать не строку, появится предупреждение PytestRemovedIn10Warning. В pytest 10 это будет TypeError.
  • Передавать bytes в TerminalReporter.write_line() больше не стоит, нужна строка. Это касается авторов плагинов.

Длинные строки в ID тестов и проверка маркеров

Появилась опция parametrize_long_str_id_strategy: она решает, что делать с длинными строками и байтами в автоматических ID тестов. По умолчанию стоит short: значения длиннее 100 символов заменяются на имя аргумента и номер. Есть ещё sha256 (хеш значения), legacy (как было, всё значение целиком) и disallow (ошибка с просьбой задать ID вручную). Явные ID через ids=[...] или pytest.param(..., id=...) это не затрагивает. Если у вас где-то выбираются тесты по старым длинным ID, верните прежнее поведение:

[tool.pytest.ini_options]
parametrize_long_str_id_strategy = "legacy"

При включённом strict_markers pytest теперь проверяет и имена маркеров в выражении -m. Опечатка вроде -m "slwo" больше не отберёт молча ноль тестов, pytest сразу укажет на неизвестный маркер. А в выводе полного диффа (Full diff) появилась легенда, какие строки помечены плюсом, а какие минусом.

Быстрее в CI: кэш assert по хешу исходника

pytest перезаписывает тестовые модули, чтобы assert показывал подробности, и кладёт результат в __pycache__. Раньше кэш сверялся по времени изменения файла. В CI после каждого checkout у всех файлов новое время, поэтому восстановленный кэш выбрасывался целиком и все модули перезаписывались заново. Теперь кэш проверяется по хешу исходника (формат pyc из PEP 552), это стоит около 15 микросекунд на файл. Старые кэши один раз пересоберутся.

Есть и другие ускорения. Захват вывода по умолчанию (--capture=fd) не читает буфер у тестов, которые ничего не печатали, а это заметно на больших наборах. Огромные диффы в assert теперь строятся лениво и сразу обрезаются, а RecursionError без найденного места рекурсии показывает только первые и последние 10 кадров вместо тысячи одинаковых. Полный стек по-прежнему доступен через --full-trace.

Мелочи, которые пригодятся

  • --no-summary теперь глушит только итог самого pytest. Сторонние плагины, например coverage, снова выводят свою сводку.
  • В pytest_plugins и PYTEST_PLUGINS можно писать имя точки входа установленного плагина, например xdist, как уже работало с -p.
  • Опции junit_family, console_output_style, tmp_path_retention_policy и ещё несколько теперь проверяют значение. Опечатка в конфиге даёт короткую ошибку вместо молчаливого игнора, а pytest --help показывает допустимые варианты.
  • Числовые опции вроде log_level, max_warnings и faulthandler_timeout в TOML можно писать числами, без кавычек.
  • Типы pytest.ParameterSet и pytest.Approx стали публичными, их можно использовать в аннотациях и isinstance.

Перед обновлением в рабочем проекте стоит прогнать тесты с -W error::pytest.PytestRemovedIn10Warning, проверить вызовы parametrize с позиционными ids или scope и посмотреть, как CI реагирует на коды выхода 3 и 4. В остальном 9.2 даёт более быстрый CI и меньше молчаливых ошибок в конфиге.

Источники и ссылки


Комментарии загружаются…