Learning Platform
Глоссарий Troubleshooting
Урок 14.01 · 28 мин
Средний
pyproject.tomlPEP 517PEP 518PEP 621PEP 680build-systembuild-backendsetuptoolshatchlingpoetry-coreflit-corepdmmaturintomllibtomli-wpy-m13-01-code-1Pattern 3

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.

В этом уроке:

  1. Зачем pyproject.toml — мотивация PEP 517 / 518 / 621 + history briefly.
  2. [build-system] table (PEP 518) — минимальные требования сборки.
  3. [project] table (PEP 621) — унифицированный metadata standard.
  4. Build backends overview — setuptools / hatchling / poetry-core / flit-core / pdm / maturin.
  5. tomllib (PEP 680, Python 3.11+ stdlib, read-only) — парсинг файлов.
  6. Pitfall 44 — Poetry pre-2.0 [tool.poetry] vs PEP 621 [project].
  7. Pitfall 45 — tomllib read-only, для записи нужен tomli-w (third-party).
  8. Code-challenge py-m13-01-code-1 — Pattern 3 parsing pyproject.toml via tomllib.loads.

Зачем pyproject.toml

До PEP-серии 517/518/621 (2016-2020) каждая build tool изобретала свой формат:

  • setuptoolssetup.py (executable) + setup.cfg (declarative, but setuptools-specific).
  • flit — собственный flit.ini.
  • poetrypyproject.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 5182016pyproject.toml файл + [build-system] table — минимальные requirements для сборки
PEP 5172017Frontend↔backend build interface — build_wheel / build_sdist API; build-system independence
PEP 6212020[project] table — унифицированные metadata (name / version / dependencies / classifiers); раньше каждый backend свой
PEP 6602021Editable installs через PEP 517 backends — pip install -e . для pyproject.toml-only projects (см. урок 03)
PEP 6802022tomllib в 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"

Два обязательных ключа:

  1. requires: list[str] — packages, нужные для сборки wheel/sdist (не runtime). Frontend (pip, uv, build) создаёт isolated venv, ставит туда requires, и зовёт build-backend.
  2. 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».

BackendModule pathКогда выбирать
setuptoolssetuptools.build_metaLegacy / migration path; зрелый; complex API; default fallback
hatchlinghatchling.buildRecommended new projects per packaging.python.org; быстрый; PEP 621 native; from PyPA
poetry-corepoetry.core.masonry.apiPoetry-managed projects; integrated с poetry CLI; pre-2.0 не понимал PEP 621, 2.0+ understands [project]
flit-coreflit_core.buildapiPure-Python простые packages; minimal config; популярен у PyPA tooling
pdm.backendpdm.backendPDM-managed projects; well-spec’d PEP 517
maturinmaturinRust 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 tomllibread-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.


В 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).

Проверьте понимание

Результат: 0 из 0
Прикладной
Вопрос 1 из 4. **Apply — `[build-system]` table:** Какие 2 обязательных ключа в `[build-system]` table (PEP 518)?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 5