Москва: проверенные б/у ноутбуки с гарантией и проверкой перед оплатой
+7 906 722-94-45

Главная → Статьи → Упаковка Python-проекта: pyproject.toml и публикация

Упаковка Python-проекта: pyproject.toml, сборка и публикация без боли

Современная упаковка Python: единый pyproject.toml вместо setup.py, выбор бэкенда сборки, версии и зависимости, сборка wheel/sdist, публикация в PyPI через trusted publishing.

Прямой ответ

Современный Python-пакет описывается одним файлом pyproject.toml (PEP 621): метаданные, зависимости, точки входа — всё там; setup.py не нужен. Сборка — python -m build (создаёт wheel и sdist), публикация — twine upload или, что правильнее в 2026-м, trusted publishing (PyPI сам доверяет вашему GitHub Actions-воркфлоу — без токенов в секретах). Выбор бэкенда сборки: setuptools (дефолт, максимум совместимости) или hatchling (проще конфигурация, активное развитие). Весь процесс от пустого проекта до устанавливаемого pip install мой-пакет занимает полчаса и окупается с первого же переиспользования кода между проектами.

Анатомия pyproject.toml

Минимальный рабочий файл содержит три секции. [build-system] — какой бэкенд собирает пакет (requires = ["setuptools>=61"], build-backend = "setuptools.build_meta"). [project] — метаданные: name, version, description, readme, requires-python = ">=3.11", dependencies = [...] (рантайм-зависимости, без версий-прищепок — они ломают установку вместе с другими пакетами; точные версии — в lock-файле приложения, не библиотеки), опциональные project.optional-dependencies (extras вроде [dev]). [project.scripts] — консольные точки входа (mytool = "mypkg.cli:main" — даёт команду mytool после установки). Инструменты (ruff, pytest, mypy) настраиваются здесь же своими секциями — один файл конфигурации на проект (настройка ruff — в статье про ruff).

Версии и зависимости: дисциплина

Версия пакета — по SemVer-интуиции (мажор — ломающие изменения, минор — новые фичи, патч — фиксы), с динамикой из git-тегов при желании (setuptools-scm/hatch-vcs). Зависимости библиотеки — с нижними границами (requests>=2.31), без верхних без нужды и без пинов: библиотека с pandas==2.1.0 становится неудаляемой из чужого окружения. Приложения — наоборот: полный lock (uv/pip-tools). Отдельная ловушка — [project] dynamic = ["version"] с источником в __init__.py: удобно, но требует дисциплины одного источника правды; версия «в двух местах» — источник «установился пакет старой версии».

Сборка и локальная проверка

pip install build && python -m build создаёт dist/*.whl и dist/*.tar.gz в изолированном окружении (ваши локальные пакеты не утекают в сборку). Проверка перед публикацией — обязательна: twine check dist/* (валидность метаданных/README — ловит битый long_description), и установка собранного wheel в чистое venv (pip install dist/*.whl в свежем окружении — проверяет, что пакеты/данные действительно попали в сборку; классический баг «работает из репозитория, не работает из wheel» — забытые package_data/подмодули). Локальные среды и venv — из статьи про окружения Python.

Публикация: TestPyPI, PyPI, trusted publishing

Порядок честный: сначала TestPyPI (отдельный индекс для прогонов — repository = "testpypi"), затем боевой PyPI. Аутентификация: API-токены в ~/.pypirc — рабочий, но токен на машине; лучший вариант — trusted publishing (OIDC): в настройках проекта PyPI привязка к GitHub-репозиторию и воркфлоу, публикация из CI без единого долгоживущего секрета (механика — PyPI Trusted Publishers; CI-воркфлоу с матрицей — в статье про GitHub Actions). Важные детали: имя проекта на PyPI занято навсегда (переиспользовать удалённое имя нельзя), повторная загрузка той же версии запрещена (даже после удаления — bump обязателен), yank версии «отзывает» её из свежих установок без лома существующих пинов.

Когда пакет, а когда просто репозиторий

Не всякий код нужно публиковать в PyPI. Внутренний переиспользуемый код между вашими проектами — приватный индекс (devpi/artifactory), git-зависимость (`pip install git+) или просто монорепо с путями. В публичный PyPI имеет смысл выходить с утилитой, решающей законченную задачу, с документацией и примерами — и готовностью поддерживать (issues, совместимость). Промежуточный шаг, который часто лучше публичного пакета — открытый репозиторий с установкой из git: та же переиспользуемость без обязательств индекса.

Практический сценарий: от скрипта к устанавливаемому пакету

Путь, который проходит большинство утилит. Этап первый — «просто скрипт»: sync.py в репозитории, запуск python sync.py, зависимости ставились руками. Боли: у коллеги другая версия библиотеки, аргументы не документированы, код не импортируется в тестах. Этап второй — структура пакета: src/synctool/__init__.py с __version__, cli.py с typer-интерфейсом, логика в отдельных модулях; в pyproject.toml — метаданные, dependencies с нижними границами и [project.scripts] sync = "synctool.cli:app"; окружение разработчика — pip install -e ".[dev]" (редактируемая установка: правки кода видны сразу, команда sync уже работает). Этап третий — качество: тесты импортируют модули напрямую (без subprocess-плясок), ruff и mypy настроены секциями того же pyproject, CI собирает wheel и прогоняет установку в чистом окружении. Этап четвёртый — дистрибуция: внутренняя команда ставит из git (pip install git+ — теги как версии), публичный релиз — сборка python -m build, проверка twine check`, тестовый прогон на TestPyPI и публикация из GitHub Actions через trusted publishing. Каждый этап здесь окупает себя сам — второй даёт тестируемость, третий — воспроизводимость, четвёртый — удобство установки для чужих людей, — и ни один не требует «сразу сделать всё правильно».

FAQ

setup.py умер? Для новых проектов — да: всё описывается в pyproject.toml, setup.py остаётся только для экзотики (C-расширения со сложной логикой сборки, legacy-проекты).

Чем wheel отличается от sdist? sdist — исходники (tar.gz, требует сборки на машине установки), wheel — готовый к распаковке архив (быстрая установка). Для чистого Python wheel универсален (py3-none-any); C-расширения требуют колёс под платформы или сборки у пользователя.

Нужен ли poetry вместо этого? Poetry закрывает и упаковку, и lock-менеджмент, но экосистема сместилась к стандартным инструментам: pyproject + build + uv (lock/среды) — меньше магии, ближе к стандартам. Poetry остаётся рабочим, но новым проектам стандартный стек проще.

Как удалить ошибочную версию с PyPI? Удалить файл версии можно, но имя и версия остаются занятыми; «отозвать» — yank (pip её не поставит без явного пина). Поэтому — TestPyPI перед боевой загрузкой и bump вместо переиздания.

Источники: Python Packaging User Guide, setuptools pyproject config, PyPI Trusted Publishers — ссылки в шапке.