Прямой ответ
Современный 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 — ссылки в шапке.