pyproject.toml — PEP 517 / 518 / 621 + build backends + tomllib
До 2016 года Python projects использовали setup.py (executable script) + setup.cfg (declarative ini-style) — мешанина исполняемого кода и метаданных. Сборка пакета фактически вызывала python setup.py install, что нарушает sandbox: install-time мог запустить произвольный код, скачивать сетевые ресурсы, пытаться компилировать. PEP 517 / 518 / 621 пошагово заменили это декларативным TOML файлом — pyproject.toml.
В этом уроке:
- Зачем
pyproject.toml— мотивация PEP 517 / 518 / 621 + history briefly. [build-system]table (PEP 518) — минимальные требования сборки.[project]table (PEP 621) — унифицированный metadata standard.- Build backends overview — setuptools / hatchling / poetry-core / flit-core / pdm / maturin.
tomllib(PEP 680, Python 3.11+ stdlib, read-only) — парсинг файлов.- Pitfall 44 — Poetry pre-2.0
[tool.poetry]vs PEP 621[project]. - Pitfall 45 —
tomllibread-only, для записи нуженtomli-w(third-party). - Code-challenge
py-m13-01-code-1— Pattern 3 parsing pyproject.toml viatomllib.loads.
Зачем pyproject.toml
До PEP-серии 517/518/621 (2016-2020) каждая build tool изобретала свой формат:
setuptools—setup.py(executable) +setup.cfg(declarative, but setuptools-specific).flit— собственныйflit.ini.poetry—pyproject.tomlс[tool.poetry](proprietary table).
Frontend tools (pip, pip-compile) жёстко знали про setup.py: install = python setup.py install. Это нарушает sandbox (executable код во время install) и привязывает к одному backend. Решение — стандартный manifest формат + protocol для frontend↔backend communication:
| PEP | Год | Что определяет |
|---|---|---|
| PEP 518 | 2016 | pyproject.toml файл + [build-system] table — минимальные requirements для сборки |
| PEP 517 | 2017 | Frontend↔backend build interface — build_wheel / build_sdist API; build-system independence |
| PEP 621 | 2020 | [project] table — унифицированные metadata (name / version / dependencies / classifiers); раньше каждый backend свой |
| PEP 660 | 2021 | Editable installs через PEP 517 backends — pip install -e . для pyproject.toml-only projects (см. урок 03) |
| PEP 680 | 2022 | tomllib в stdlib (Python 3.11+) — read-only TOML parser |
Результат: pyproject.toml — единственный файл с manifest + build config; install-time не запускает Python код; pip / uv / build / poetry все honor PEP 517 protocol.
Cite peps.python.org/pep-0517 + peps.python.org/pep-0518 + peps.python.org/pep-0621.
[build-system] table (PEP 518)
Минимальный валидный pyproject.toml — это только [build-system]:
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
Два обязательных ключа:
requires: list[str]— packages, нужные для сборки wheel/sdist (не runtime). Frontend (pip,uv,build) создаёт isolated venv, ставит тудаrequires, и зовётbuild-backend.build-backend: str— dotted path к backend module — реализует PEP 517 entry-points (build_wheel,build_sdist, опц.build_editable).
Mental model:
[build-system]отвечает на вопрос «как этот проект собрать в.whl?» Pip/uv не должны знать setuptools-specifics — они вызываютhatchling.build.build_wheel(...)через PEP 517 API.
Without [build-system] modern frontends (pip ≥21.3) предполагают legacy fallback к setuptools — но это deprecated path. Best practice: всегда явно указывать backend.
[project] table (PEP 621)
Сердце manifest — [project] table (унифицированный standard с 2020):
[project]
name = "myapp"
version = "1.0.0"
description = "Demo packaging project"
readme = "README.md"
requires-python = ">=3.11"
authors = [
{ name = "Alice Example", email = "[email protected]" },
]
license = { text = "MIT" }
keywords = ["packaging", "demo"]
classifiers = [
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
]
dependencies = [
"httpx>=0.25",
"pydantic>=2.0",
"click",
]
Полный список ключей — name / version / description / readme / requires-python / dependencies / optional-dependencies / dynamic / authors / maintainers / license / keywords / classifiers / urls / scripts / gui-scripts / entry-points. Все они declarative — backends сами читают и применяют.
Совместимость: [project] table — единственный cross-tool standard. setuptools ≥61.0, hatchling, poetry-core ≥2.0, flit-core, pdm.backend все понимают [project]. Это важный аргумент в пользу миграции на [project] от proprietary tables (см. Pitfall 44).
Cite packaging.python.org/en/latest/guides/writing-pyproject-toml/.
Build backends overview
Backend — реализация PEP 517 build protocol; конкретный module отвечает за «как [project] превращается в .whl».
| Backend | Module path | Когда выбирать |
|---|---|---|
| setuptools | setuptools.build_meta | Legacy / migration path; зрелый; complex API; default fallback |
| hatchling | hatchling.build | Recommended new projects per packaging.python.org; быстрый; PEP 621 native; from PyPA |
| poetry-core | poetry.core.masonry.api | Poetry-managed projects; integrated с poetry CLI; pre-2.0 не понимал PEP 621, 2.0+ understands [project] |
| flit-core | flit_core.buildapi | Pure-Python простые packages; minimal config; популярен у PyPA tooling |
| pdm.backend | pdm.backend | PDM-managed projects; well-spec’d PEP 517 |
| maturin | maturin | Rust extensions через pyo3; генерирует platform-specific wheels |
Decision rule: новый pure-Python project → hatchling.build (cite hatch.pypa.io). Существующий setuptools project → миграция опциональна. Rust extension → maturin.
Recipe — minimal pyproject.toml ready для
python -m build(cross-link урок 05):[build-system] requires = ["hatchling>=1.18"] build-backend = "hatchling.build" [project] name = "demo" version = "0.1.0" requires-python = ">=3.11" dependencies = [] [tool.hatch.build.targets.wheel] packages = ["src/demo"]
tomllib (PEP 680, Python 3.11+ stdlib)
Python 3.11+ ships tomllib — read-only TOML 1.0 parser в stdlib. До 3.11 нужен был third-party tomli (тот же author, Sebastián Ramírez ускорял через C bindings).
import tomllib
with open("pyproject.toml", "rb") as f: # ВСЕГДА binary mode
cfg = tomllib.load(f)
print(cfg["project"]["name"])
print(cfg["project"]["dependencies"])
print(cfg["build-system"]["build-backend"])
Альтернативно — tomllib.loads(toml_str) принимает уже decoded string:
import tomllib
cfg = tomllib.loads("""
[project]
name = "demo"
""")
print(cfg["project"]["name"]) # 'demo'
tomllib returns plain dict — ни magic ни pydantic-like validation. Доступ как обычная nested dict. Тут наш мост к M07 урок 04 (runtime introspection): после tomllib.loads(...) мы можем структурировать payload в dataclass через dataclasses.asdict (rev) или просто читать cfg['project']['dependencies'] как list[str]. Mental model: cfg ≈ structured data — то же, что и json.loads (M09 урок 03).
Cite peps.python.org/pep-0680 + docs.python.org/3/library/tomllib.html.
Pitfall 45 — tomllib read-only
tomllib.dumps не существует:
import tomllib
tomllib.dumps({"project": {"name": "x"}})
# AttributeError: module 'tomllib' has no attribute 'dumps'
PEP 680 намеренно ship’ает только parser — стандартизация writing TOML отложена (нужно решить comment preservation, key ordering, multiline strings). Для записи используйте third-party tomli-w:
# pip install 'tomli-w>=1.0' (вне Pyodide — Run-on-Your-Machine)
import tomli_w
with open("out.toml", "wb") as f:
tomli_w.dump({"project": {"name": "x"}}, f)
В наших challenges мы parsing pyproject.toml через tomllib.loads — никогда dumps.
Pitfall 44 — Poetry pre-2.0 [tool.poetry] vs PEP 621 [project]
До Poetry 2.0 (январь 2025) Poetry projects использовали proprietary [tool.poetry] table:
# Poetry pre-2.0 — НЕ standard, понимает только Poetry
[tool.poetry]
name = "myapp"
version = "1.0.0"
[tool.poetry.dependencies]
python = "^3.11"
httpx = "^0.25"
Этот формат не понимали другие tools — pip install через PEP 517 видел [build-system] с poetry-core, но не имел доступа к metadata из [tool.poetry]. Result — Poetry users эффективно были привязаны к Poetry CLI.
Poetry 2.0 добавил поддержку [project] (PEP 621). Recommended миграция:
# Poetry 2.0+ — cross-tool compatible
[project]
name = "myapp"
version = "1.0.0"
requires-python = ">=3.11"
dependencies = ["httpx>=0.25"]
[build-system]
requires = ["poetry-core>=2.0"]
build-backend = "poetry.core.masonry.api"
Recommend для новых projects: [project] всегда; [tool.poetry] оставить только для Poetry-specific config (sources, scripts), где PEP 621 не покрывает. Cite python-poetry.org/docs/pyproject.
Cross-link M07 урок 04 — runtime introspection of structured data
В M07 урок 04 мы рассматривали dataclass introspection — dataclasses.fields(cls), dataclasses.asdict(instance), runtime __annotations__. После tomllib.loads(toml_str) у нас обычный nested dict — но pattern доступа тот же:
import tomllib
cfg = tomllib.loads(toml_text)
# Field access по path — параллель с dataclass attribute access:
name = cfg["project"]["name"] # → str
deps = cfg["project"]["dependencies"] # → list[str]
backend = cfg["build-system"]["build-backend"] # → str
Можно построить dataclass поверх:
from dataclasses import dataclass
@dataclass
class ProjectMeta:
name: str
version: str
dependencies: list[str]
raw = tomllib.loads(toml_text)["project"]
meta = ProjectMeta(
name=raw["name"],
version=raw["version"],
dependencies=raw.get("dependencies", []),
)
Тот же pattern, что в M07 урок 04 (dataclass field introspection) — tomllib.loads даёт dict, мы материализуем в typed structure. Cross-link → 07-type-hints/04-runtime-introspection.
Code-challenge py-m13-01-code-1 — Pattern 3 (pyproject.toml parsing via tomllib)
Задача (Pattern 3 — function-call mode): написать solve(toml_str), который parsing pyproject.toml через tomllib.loads(...) и возвращает tuple (name, count_dependencies, build_backend). Если [project] отсутствует — name '<unnamed>'. Если [build-system] отсутствует — backend '<no-backend>'. Если dependencies отсутствует — count = 0.
Pyodide ships Python 3.12 → tomllib доступен в stdlib (PEP 680 verified). Тестируется 3 testCases (1 hidden).
Pyodide-friendly: pure stdlib
tomllib.loads. Никакихpip install,subprocess.run,tomllib.dumps(Pitfall 45) — только read-side API.
Полный challenge embedded в quiz py-m13-01-q* (см. M13 урок 01 quiz). Solution outline:
import tomllib
def solve(toml_str):
cfg = tomllib.loads(toml_str)
name = cfg.get("project", {}).get("name", "<unnamed>")
deps = cfg.get("project", {}).get("dependencies", [])
backend = cfg.get("build-system", {}).get("build-backend", "<no-backend>")
return (name, len(deps), backend)
Cite docs.python.org/3/library/tomllib.html.
Recipe end-to-end — read pyproject.toml + emit summary
import tomllib
from pathlib import PurePosixPath
def summarize(path: str) -> dict[str, str | int]:
with open(path, "rb") as f:
cfg = tomllib.load(f)
project = cfg.get("project", {})
build = cfg.get("build-system", {})
return {
"name": project.get("name", "<unnamed>"),
"version": project.get("version", "0.0.0"),
"deps_count": len(project.get("dependencies", [])),
"requires_python": project.get("requires-python", "*"),
"backend": build.get("build-backend", "<none>"),
}
if __name__ == "__main__":
summary = summarize("pyproject.toml")
for k, v in summary.items():
print(f"{k}: {v}")
Этот pattern — основа для CI/CD scripts: python -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])" — извлечь version в bash variable. Pure stdlib, zero dependencies.
Что в следующем уроке
Урок 02 — package managers: pip / uv / Poetry / rye (deprecated Feb 2025 — Pitfall 46). Resolver semantics — pip backtracking vs uv pubgrub. Run-on-Your-Machine #1 — host uv pip install timing demo (~10-30× быстрее pip).