pyproject.toml вместо setup.py: собираем пакет по-новому
setup.py больше не нужен для сборки: PEP 517/518 сделали pyproject.toml единым файлом конфигурации. Разбираем, что туда писать и чем платишь за переход на build-бэкенды.

Если вы открываете свежий репозиторий пакета в 2024 году и не находите там setup.py, это не забывчивость автора. Начиная с PEP 517 и PEP 518 (приняты в 2017–2018 годах) метаданные и настройки сборки собираются в одном файле — pyproject.toml. А PEP 621 (2020) стандартизировал, как именно писать метаданные проекта, чтобы они не зависели от конкретного инструмента сборки. Разберём, чем этот подход отличается от привычного setup.py, что вы выигрываете и где придётся заплатить временем на разбирательство.
Почему setup.py перестал быть единственным способом
Классический setup.py — это исполняемый Python-скрипт. Чтобы прочитать даже имя и версию пакета, инструмент установки должен был запустить произвольный код. Это создавало две проблемы: непредсказуемость (скрипт мог делать что угодно) и курицу с яйцом — для сборки нужен setuptools, но откуда система узнает, какой версии и что он вообще нужен.
PEP 518 ввёл секцию [build-system]: там декларативно указано, какие пакеты нужны для сборки. PEP 517 задал стандартный интерфейс между фронтендом (например, pip или build) и бэкендом сборки (setuptools, hatchling, flit, poetry-core). Теперь фронтенду не важно, чем собран пакет, — он вызывает стандартные функции бэкенда.
Главная перемена не в синтаксисе, а в модели: раньше пакет описывал сам себя исполняемым кодом, теперь — декларативными данными, которые можно прочитать, не запуская.
Что кладём в pyproject.toml
Файл состоит из трёх ключевых секций. Минимальный рабочий пример для setuptools как бэкенда:
[build-system]
requires = ["setuptools>=61.0", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "myawesomelib"
version = "0.1.0"
description = "Библиотека для разбора накладных"
readme = "README.md"
requires-python = ">=3.9"
license = { text = "MIT" }
authors = [
{ name = "Имя Автора", email = "author@example.com" },
]
dependencies = [
"requests>=2.28",
"pydantic>=2.0",
]
[project.optional-dependencies]
dev = ["pytest>=7.0", "ruff"]
[project.scripts]
myawesome = "myawesomelib.cli:main"
Разберём, что здесь происходит:
- [build-system] — единственная секция, которую читает фронтенд до всего остального.
requires— что поставить в изолированное окружение сборки.build-backend— точка входа бэкенда. Поддержка секции[project]в setuptools появилась в версии 61.0, поэтому нижняя граница именно такая. - [project] — метаданные по PEP 621: имя, версия, зависимости. Эти поля читаются одинаково любым PEP 621-совместимым бэкендом.
- [project.optional-dependencies] — необязательные группы, которые ставятся как
pip install myawesomelib[dev]. - [project.scripts] — консольные команды: после установки в PATH появится
myawesome, вызывающийmainизmyawesomelib.cli.
Сноска: build-бэкенд
Build-бэкенд — это пакет, который умеет превращать исходники в дистрибутив (wheel и sdist). Фронтенд (pip, build) вызывает у бэкенда стандартные функции вроде build_wheel. Setuptools, hatchling, flit-core, poetry-core — всё это бэкенды. Вы выбираете один и указываете его в build-backend.
Собираем и проверяем
Для сборки используется отдельный пакет build — официальный референсный фронтенд от PyPA. Он создаёт изолированное окружение, ставит туда то, что перечислено в requires, и вызывает бэкенд.
# ставим фронтенд сборки (проверено с build 1.x)
python -m pip install --upgrade build
# запускаем сборку из корня проекта
python -m build
# в каталоге dist/ появятся два файла:
# myawesomelib-0.1.0.tar.gz — sdist (исходники)
# myawesomelib-0.1.0-py3-none-any.whl — wheel
Проверка результата: в каталоге dist/ лежат оба файла — архив с исходниками и wheel. Если сборка прошла, в конце вывода будет строка вида Successfully built myawesomelib-0.1.0.tar.gz and myawesomelib-0.1.0-py3-none-any.whl. Для локальной разработки ставьте пакет в editable-режиме — это тоже работает через pyproject.toml:
# editable-установка, изменения в коде подхватываются без переустановки
python -m pip install -e .
# с dev-зависимостями
python -m pip install -e ".[dev]"
Где в пакете лежит код: два лэйаута
Setuptools не всегда угадывает, какие пакеты включать. Есть два подхода к структуре каталогов, и выбор влияет на надёжность тестов.
| Параметр | flat layout | src layout |
|---|---|---|
| Расположение кода | пакет в корне проекта | пакет внутри src/ |
| Импорт из корня без установки | работает случайно | невозможен — и это плюс |
| Риск тестировать неустановленный код | высокий | отсутствует |
| Автообнаружение setuptools | иногда цепляет лишнее (tests, docs) | берёт только содержимое src |
src layout заставляет тесты работать с установленным пакетом, а не с локальными файлами, — это ближе к тому, что получит пользователь. Для явного указания при src layout добавьте:
[tool.setuptools.packages.find]
where = ["src"]
Версия из одного места
Дублировать версию в pyproject.toml и в __init__.py — источник рассинхрона. Setuptools умеет читать версию динамически. Объявите поле динамическим и укажите, откуда его брать:
[project]
name = "myawesomelib"
dynamic = ["version"]
# остальные поля
[tool.setuptools.dynamic]
version = { attr = "myawesomelib.__version__" }
Теперь версия хранится только в коде пакета, а сборка её оттуда вытаскивает. Обратите внимание: поле version нельзя одновременно задать статически в [project] и объявить в dynamic — будет ошибка.
Если не получилось
- ERROR Multiple top-level packages discovered in a flat-layout — setuptools нашёл больше одного каталога-кандидата (например,
myawesomelibиtests). Перейдите на src layout или явно задайте пакеты через[tool.setuptools.packages.find]. - error: Multiple .egg-info directories found или странные метаданные — остались артефакты от старого
setup.py develop. Удалите*.egg-info,build/,dist/и соберите заново. - Invalid value for requires-python или падение при чтении
[project]— скорее всего setuptools старше 61.0. Проверьтеpython -m pip show setuptoolsи поднимите нижнюю границу вrequires. - ModuleNotFoundError после pip install -e . при src layout — не задан
where = ["src"], и бэкенд не нашёл пакет. Добавьте секцию find. - version dynamic ... but no such attribute — путь в
attrуказывает на несуществующую переменную. Проверьте, что__version__действительно объявлена в__init__.pyпакета.
Нужен ли setup.py вообще
Для чистой сборки — нет. Но пустой setup.py с единственной строкой from setuptools import setup; setup() иногда оставляют для совместимости со старыми инструментами, которые ещё не научились работать через PEP 517. Если ваша цепочка сборки современная (pip 21.3+, build), файл не нужен. setup.cfg тоже уходит: всё, что раньше писали в нём, теперь живёт в pyproject.toml.
Что делать дальше
Соберите пакет через python -m build и проверьте содержимое wheel командой python -m zipfile -l dist/*.whl — там должны быть только нужные модули, без тестов и мусора. Перед публикацией на PyPI прогоните twine check dist/*, чтобы поймать проблемы с метаданными и README до загрузки. Если проект перерастает setuptools по скорости или удобству, посмотрите на hatchling — он тоже читает секцию [project], так что метаданные при смене бэкенда переписывать не придётся, меняется только [build-system].
Контент-конвейер: Из одной идеи — текст, изображение, озвучка и ролик. Один заказ вместо четырёх.
Спросить за 60 ₽Источники: PEP 621 — Storing project metadata in pyproject.toml, Setuptools — Configuring setuptools using pyproject.toml files
Частые вопросы
У меня setuptools старой версии, секция [project] не читается. Что делать?
Нужно ли удалять старый setup.py при переходе?
Как задать зависимости, которые нужны только для разработки?
Что выбрать бэкендом — setuptools, hatchling или poetry-core?
Сборка падает с ошибкой про multiple top-level packages. Почему?
Можно ли не дублировать номер версии в коде и в pyproject.toml?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.