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

Как я начинаю разработку Python-библиотеки для продакшена в 2026 году

Каждая библиотека этой серии начиналась одинаково: одна команда, сорок один файл и зелёные проверки примерно через три секунды. Речь о распределении усилий. Решения, обеспечивающие удобство сопровождения, — что проверять, кто меняет версию, как публиковать и для кого писать документацию — принимаются один раз и применяются шаблоном. Поэтому работа над девятой библиотекой — сама библиотека, а не её обвязка.

Всё ниже сгенерировано и запущено сегодня с python-library-template на Python 3.13.

Одна команда

uvx --with jinja2-time copier copy \
  --data project_name="Widget Kit" --data package_name="widget_kit" \
  gh:bedrock-python/python-library-template ./widget-kit

Сорок один файл, проверки запускаются ещё до первой написанной мной строки:

    uv run ruff check .           All checks passed!
    uv run ruff format --check .  8 files already formatted
    uv run mypy widget_kit        Success: no issues found in 2 source files
    make check                    2.25s
    pytest ...                    1 passed, coverage 100.00% (threshold 90%)
    make test                     0.66s

Три секунды. Это важнее, чем кажется: быстрые проверки запускают перед push, а предотвращают проблемы только те проверки, которые действительно запускают.

Что входит в сорок один файл

Примерно восемь файлов настройки упаковки и инструментов, четыре workflow GitHub, пять файлов для участников проекта, дерево docs/ с семью страницами, тесты и сам пакет из трёх файлов.

Интересно, какие решения принимает каждая группа.

Проверки. ruff для линтинга и форматирования, строгий mypy для пакета, pytest с минимальным покрытием 90% и четырнадцать хуков pre-commit, включая conventional commits. Эти решения не хочется принимать заново в каждом репозитории и тем более допускать их расхождение: библиотека с рекомендательным mypy и библиотека, где он блокирует изменения, — разные вещи.

Workflow. Линтинг, модульные тесты на поддерживаемых версиях Python, интеграционные тесты и задача all-checks-passed, служащая единственной обязательной проверкой. Полезный небольшой приём: защита ветки ссылается на одно имя задачи, поэтому добавление версии Python в матрицу не требует менять правила репозитория.

Релиз. Автоматизация на основе conventional commits, версия ровно в одном файле, который обновляет автоматизация, и публикация в PyPI через OIDC без токенов. Полная схема — в статье о публикации.

Документация. Семь страниц, одна из них docs/agents.md: всё, что нужно ассистенту для правильного использования библиотеки, на одной странице. Организация приняла это соглашение после наблюдений за ошибочными догадками об API. Этому посвящена отдельная статья. Формат включён в шаблон, потому что иначе он останется особенностью одного репозитория.

Проверки должны делать то, что обещают

Сегодняшняя генерация обнаружила проблему, стоившую всего упражнения. Конфигурация линтера выбирала семейство DTZ, запрещающее datetime.now() без часового пояса, а затем добавляла DTZ в исключения вместе с ANN. Выбрано и отключено в одном блоке, во всех библиотеках из шаблона.

Сам код от этого не сломался. Сломалось обещание: человек или ассистент, читающий файл, решил бы, что datetime без часового пояса проверяются. Для библиотек о дедлайнах, сроках хранения, границах партиций и TTL это особенно нежелательное молчаливое отключение.

Исправление — две строки и комментарий, явно формулирующий правило:

# Everything selected here runs. An entry below names one rule the house style
# disagrees with -- never a whole family that `select` has just asked for, which
# reads as a promise the linter does not keep.
select = ["F", "E", "W", "I", "B", "N", "S", "C4", "DTZ", "SIM", "TRY", "PERF", "RUF", "UP", "ANN", ...]
ignore = ["TRY003", "ANN401", "RUF012", "S104", "ANN204", "N802", "PERF401", "SIM105", "S607"]

[tool.ruff.lint.per-file-ignores]
"tests/**/*.py" = ["S101", "S105", "S106"]

Раньше целиком отключённые семейства включены. Конкретные правила, с которыми расходится стиль проекта, перечислены отдельно. Исключения, осмысленные только в тестах, — assert, встроенные пароли fixture — помещены в блок по файлам. Теперь datetime.now() без часового пояса ломает проверку:

    3 |     return datetime.datetime.now()
      |            ^^^^^^^^^^^^^^^^^^^^^^^
    help: Pass a `datetime.timezone` object to the `tz` parameter

В CI шаблона появился шаг: сгенерировать проект, добавить datetime без часового пояса и функцию без аннотаций и завершиться ошибкой, если линтер промолчит. Конфигурации, обещающей проверку, нужен тест этого обещания, иначе оно снова перестанет выполняться.

Новый набор правил не нашёл таких datetime в четырёх существующих библиотеках. Это хорошо, но не делает отключение безвредным: дисциплина соблюдалась без помощи инструмента, а на это нельзя строить план.

Чего шаблон не даст

Причину существовать. Каждая библиотека серии выросла из измеренной проблемы сервиса, а не желания написать библиотеку. Бесплатная обвязка повышает требования к её появлению: если единственный аргумент — потенциальная переиспользуемость кода, ему место в сервисе, пока он не понадобится второй раз.

Публичный API, с которым удобно жить. __all__ — обещание, которого шаблон за вас не даст. Он делает его видимым: страницу agents с перечнем публичного API неудобно писать, когда API случаен. На версии 0.1 это полезный дискомфорт.

Интеграционные тесты с настоящей зависимостью. Шаблон разделяет модульные и интеграционные проверки на задачи, а контейнеры добавляете вы. Всё достойное статьи в этой серии обнаружилось в тестах с настоящими PostgreSQL, Kafka и Redis. Библиотека, чья интеграция проверяется только моками, узнает своё поведение в production.

В каком порядке я работаю

  1. Зафиксировать проблему падающим тестом с настоящей зависимостью в сервисе, где она мешает.
  2. Сгенерировать библиотеку и перенести минимальное решение вместе с тестом.
  3. Написать страницу agents раньше руководства. Она заставляет превратить публичный API в список имён, который можно удержать в голове.
  4. Сразу выпустить 0.1.0: версия, прошедшая публикацию, ценнее идеальной версии, которая её не проходила.
  5. Использовать библиотеку в сервисе, а следующую версию определить следующей проблемой.

Шаблон почти устраняет работу на втором и четвёртом шагах. Остальное и есть основная задача.

ИДЕЯ В СХЕМЕПодготовьте проверки до роста библиотеки
---
config:
  theme: default
  look: classic
  flowchart:
    useMaxWidth: false
    wrappingWidth: 150
    padding: 12
    nodeSpacing: 24
    rankSpacing: 32
---
flowchart TD
    accTitle: Подготовьте проверки до роста библиотеки
    accDescr: Шаблон даёт упаковку, CI и структуру документации. Тесты и реальный сервис-потребитель показывают, работает ли дизайн библиотеки на практике.
 T["Copier"] --> G["Создать проект"] --> C["Запустить проверки качества"] --> U["Реализовать и проверить ядро"] --> D["Проверить в реальном сервисе"]

Шаблон даёт упаковку, CI и структуру документации. Тесты и реальный сервис-потребитель показывают, работает ли дизайн библиотеки на практике.

Инструменты

python-library-template — шаблон copier и установочный скрипт, применяющий к новому репозиторию защиту ветки, обязательную проверку, dependabot и окружение релиза. Из него созданы все библиотеки серии, поэтому исправление конфигурации линтинга исправляет сразу двенадцать репозиториев после получения обновления.

Сорок один файл, три секунды и одна строка конфигурации, которая обещала неправду.