pyproject.toml-Leitfaden: Packaging, Abhängigkeiten und Tools

Automatische Übersetzung

Dieser Artikel wurde automatisch aus der englischen Originalversion übersetzt.

Ältere Python-Projekte verteilen ihre Konfiguration oft auf setup.py, setup.cfg, Requirements-Dateien, MANIFEST.in und einzelne Dateien für Entwicklungswerkzeuge. pyproject.toml gibt diesen Aufgaben einen gemeinsamen Ort. Es ersetzt aber nicht jede Projektdatei, und nicht jeder Abschnitt gehört zu einem einzigen Standard.

Dieser Leitfaden richtet sich an Python-Entwickler, die ein Package oder eine Anwendung pflegen. Du lernst, was die vier wichtigsten Tabellen enthalten ([build-system], [project], [dependency-groups], [tool.*]), wie sie sich von einem Lockfile und von requirements.txt unterscheiden, welches Build-Backend du wählen solltest und wie du ein älteres Projekt migrierst und ein neues mit uv aufsetzt.

Eine Datei, vier Tabellen

Drei Packaging-Standards legen die Grundstruktur fest:

  • PEP 518 definiert die Anforderungen an das Build-System.
  • PEP 621 definiert die Projekt-Metadaten.
  • PEP 735 definiert unveröffentlichte Dependency Groups.

Tool-Autoren können außerdem einen Namensraum unter [tool] beanspruchen. Was dort steht, ist nicht standardisiert: Jedes Tool definiert seine eigenen Schlüssel und sein eigenes Verhalten.

Die Abbildung zeigt, welches Programm welche Tabelle liest. Nur [project] erreicht die Nutzer, und zwar über die Metadaten im gebauten Wheel.

Die vier Tabellen in pyproject.toml und welches Programm jede liestDie vier Tabellen in pyproject.toml und welches Programm jede liest

Hier ist eine kleine Bibliothek als Package:

[build-system]
requires = ["hatchling>=1.26"]
build-backend = "hatchling.build"

[project]
name = "weather-client"
version = "0.1.0"
description = "A small weather API client"
readme = "README.md"
requires-python = ">=3.11"
dependencies = [
  "httpx>=0.27",
]

[project.optional-dependencies]
cli = ["rich>=13"]

[project.scripts]
weather = "weather_client.cli:main"

[dependency-groups]
test = ["pytest>=8", "pytest-cov>=5"]
lint = ["ruff>=0.12"]

[tool.pytest.ini_options]
testpaths = ["tests"]

[tool.ruff]
line-length = 100
target-version = "py311"

Die Datei enthält vier Abhängigkeitslisten. dependencies führt auf, was das Package zur Laufzeit braucht, optional-dependencies was Nutzer zusätzlich wählen können, [dependency-groups] was Mitwirkende lokal installieren, und [build-system].requires was das Backend zum Bauen braucht.

[build-system]: wie aus dem Quellcode eine Distribution wird

Ein Build-Frontend wie python -m build, pip oder uv ruft ein Build-Backend auf. Das Backend entscheidet, wie der Quellbaum zu einem sdist oder Wheel wird und welche Dateien in diese Artefakte gelangen. Im Beispiel nennt build-backend hatchling, und requires führt auf, was das Frontend installiert, um es auszuführen.

Ein Build läuft in dieser Reihenfolge ab. Das Frontend liest [build-system], installiert requires in eine temporäre Build-Umgebung und startet das Backend. Das Backend liest [project] und schreibt das sdist und das Wheel.

requires enthält die Abhängigkeiten, die nötig sind, um das Backend in seiner isolierten Build-Umgebung auszuführen. Es ist nicht die Liste der Laufzeitabhängigkeiten deines Packages.

Deklariere ein Build-System, wenn das Projekt eine Distribution erzeugt oder wenn der eigene Code als Package installiert werden muss. Ein Projekt ohne Package kann pyproject.toml trotzdem nutzen; PEP 735 erlaubt sogar eine Datei, die nur Dependency Groups enthält. Ohne Build-System installiert uv das Projekt nicht in seine eigene Umgebung: Der eigene Code lässt sich nur vom Repository-Root aus importieren, und uv erzeugt keine [project.scripts]-Befehle. Setze tool.uv.package = true oder füge ein Build-System hinzu, wenn du eines von beiden brauchst (uv project packaging).

Ein Backend wählen

Wähle das Backend danach, was der Build leisten muss:

BackendWann es passt
uv_buildDas Projekt ist reines Python, wird mit uv verwaltet und braucht keine Build-Plugins. uv init schreibt es.
hatchlingDas Projekt ist reines Python und braucht Build-Plugins oder eine Version aus git (hatch-vcs).
flit-coreDas Projekt ist das einfachste reine Python-Package und braucht keine Build-Plugins.
setuptoolsEin bestehendes setup.py, C-Extensions oder ein eigener Build-Schritt müssen weiter funktionieren.
poetry-coreDas Team verwaltet das Projekt mit Poetry.
maturinDie Extension ist in Rust geschrieben.
scikit-build-coreDie Extension ist in C oder C++ geschrieben und wird mit CMake gebaut.
meson-pythonDas Projekt wird mit Meson gebaut, wie NumPy und SciPy.

Die Abbildung stellt die Fragen der Reihe nach: zuerst kompilierter Code, dann der Bedarf an Build-Plugins, dann das Tool, das das Projekt verwaltet. uv_build hat kein Plugin-System, daher braucht ein uv-Projekt, das seine Version aus git-Tags bezieht, weiterhin hatchling.

Welches Build-Backend passtWelches Build-Backend passt

Kopiere die aktuell empfohlene Tabelle des Backends aus dessen Dokumentation. Füge wheel nicht aus Gewohnheit zu den Build-Anforderungen hinzu; das Backend deklariert selbst, was es braucht.

[project]: Metadaten für die Konsumenten

Die Tabelle [project] beschreibt die Distribution: Name, Version, Python-Kompatibilität, Laufzeitabhängigkeiten, Entry Points und weitere Index-Metadaten.

Die Dependency Specifier, etwa httpx>=0.27 im Beispiel, sind Bedingungen für einen Resolver und kein Abbild einer einzelnen Umgebung. Sie werden Teil der Metadaten von Wheel und sdist, sodass nachgelagerte Installer sie mit den Anforderungen anderer Packages kombinieren können.

Extras sind eine öffentliche Installationsschnittstelle

[project.optional-dependencies] definiert Extras, die Konsumenten anfordern können. Mit dem Extra cli aus dem Beispiel kann ein Konsument weather-client[cli] installieren. Weil Extra-Namen und Anforderungen veröffentlicht werden, behandle sie als Produktfunktionen und benenne sie entsprechend, etwa cli oder postgres. Nutze kein Extra namens dev, nur um alle Werkzeuge der Mitwirkenden zu sammeln.

Entry Points verbinden installierte Befehle mit Python

Die Tabelle [project.scripts] im Beispiel hat einen Eintrag: weather = "weather_client.cli:main". Nach der Installation der Distribution stellt die Umgebung einen Befehl weather bereit, der weather_client.cli importiert und main aufruft. Der Befehl weather wird auch ohne das Extra cli installiert. Deshalb muss cli.py ohne rich funktionieren oder dem Nutzer sagen, dass er weather-client[cli] installieren soll.

Teste den Befehl aus einem gebauten Wheel und nicht nur aus dem Repository-Root; das Wheel erhalten die Nutzer. Der Abschnitt zur Migration zeigt die Befehle.

[dependency-groups]: lokale, unveröffentlichte Umgebungen

PEP-735-Dependency-Groups beschreiben Entwicklungs- oder Nicht-Package-Umgebungen, ohne sie als Package-Metadaten zu veröffentlichen.

[dependency-groups]
test = ["pytest>=8", "pytest-cov>=5"]
lint = ["ruff>=0.12"]
dev = [
  { include-group = "test" },
  { include-group = "lint" },
]

Hier gehören Tests, Linter, Dokumentations-Builder und ähnliche Werkzeuge der Mitwirkenden hin. Sie eignen sich auch für Anwendungen oder Notebooks, die keine Distribution bauen. Mit include-group kann eine Gruppe eine andere wiederverwenden; Extras kennen dafür kein Gegenstück.

PEP 735 definiert keine Kommandozeilenschnittstelle, daher hat jeder Installer eigene Flags. Gruppen und Extras werden so installiert:

# Dependency groups (not published)
pip install --group test        # pip 25.1 and later
uv sync --group test
uv sync --all-groups

# Extras (published)
pip install "weather-client[cli]"
uv sync --extra cli

pyproject.toml deklariert Abhängigkeiten, erzeugt aber keine Umgebung. Die Befehle, die eine Umgebung anlegen und das Projekt mit einer Gruppe installieren, unterscheiden sich je nach Tool:

WorkflowUmgebung anlegenProjekt und die Gruppe test installieren
pippython -m venv .venv, dann aktivierenpip install -e . --group test
uvuv sync legt .venv an, falls es fehltuv sync --group test (installiert auch das Projekt und dev)

[tool.*]: Konfiguration, die einem Tool gehört

Tool-Tabellen teilen kein gemeinsames Schema. Im Beispiel nutzen [tool.pytest.ini_options] und [tool.ruff] jeweils Schlüssel, die nur das eigene Tool liest. Nutze eine [tool.*]-Tabelle nur, wenn das Tool sie dokumentiert. Diese verbreiteten Tools lesen ihre Einstellungen aus pyproject.toml:

AufgabeToolTabelle
Formatierung und LintingRuff[tool.ruff]
Testspytest[tool.pytest.ini_options] oder [tool.pytest]
Typprüfungmypy[tool.mypy]
Coveragecoverage.py[tool.coverage.run], [tool.coverage.report]

pytest 9.0 hat [tool.pytest] eingeführt, das native TOML-Typen wie eine Liste für addopts nutzt; ältere Versionen lesen nur [tool.pytest.ini_options].

Manche Einstellungen gehören weiterhin in eigene Dateien, weil ein anderes Programm sie liest oder die Datei ein anderes Format braucht. flake8 liest pyproject.toml nicht; es liest .flake8, setup.cfg oder tox.ini. pre-commit liest .pre-commit-config.yaml.

Deklarationen, Locks und Requirements-Dateien lösen verschiedene Probleme

Wenn du ein Package hinzufügst, zeigt die Abbildung, in welche Liste es gehört. Stelle die Fragen für jedes Package von oben nach unten.

Wohin eine neue Abhängigkeit gehörtWohin eine neue Abhängigkeit gehört

Fünf Stellen halten Abhängigkeitsinformationen. Jede hat eine Aufgabe:

ArtefaktHauptzweckTypischer Inhalt
[project.dependencies]Veröffentlichter LaufzeitvertragDirekte Anforderungen und kompatible Bereiche
[project.optional-dependencies]Veröffentlichte optionale FunktionenExtras für Konsumenten
[dependency-groups]Unveröffentlichte lokale UmgebungenGruppen für Tests, Linting, Doku oder Anwendungen
LockfileEine aufgelöste Umgebung reproduzierenExakte Versionen, Quellen und Metadaten der Auflösung
requirements.txtEingabe für die Installation mit pipAnforderungen plus pip-spezifische Optionen, Constraints, URLs oder Hashes

Eine Bibliothek veröffentlicht normalerweise kompatible Bedingungen und testet gegen einen Bereich. Eine Anwendung committet üblicherweise das Lockfile des Umgebungsmanagers. Exakte Pins gehören in das aufgelöste Deployment-Artefakt, nicht blind in die öffentlichen Metadaten einer Bibliothek. Eine Bibliothek deklariert zum Beispiel httpx>=0.27 und überlässt die Versionswahl dem Resolver des Nutzers. Eine Anwendung committet uv.lock und installiert es mit uv sync --locked.

Behalte eine Requirements-Datei, wenn eine Integration das Format oder die Funktionen von pip verlangt. Braucht ein uv-verwaltetes Projekt eine, exportiere sie aus dem Lock, statt zwei unabhängige Abhängigkeitssätze zu pflegen:

uv export --format requirements.txt --output-file requirements.txt

Der Export ist abgeleitete Ausgabe für die Kompatibilität. Das Lock bleibt für diesen Workflow die aufgelöste Quelle.

Eine Migration, die das Verhalten erhält

Beginne nicht damit, setup.py oder requirements.txt zu löschen. Ordne zuerst ein, was jede bestehende Datei tut. Die Abbildung zeigt die Reihenfolge der folgenden Schritte und die Rückkehr zur Backend-Wahl, wenn das gebaute Wheel abweicht.

Reihenfolge der Migration für ein bestehendes ProjektReihenfolge der Migration für ein bestehendes Projekt

  1. Erfasse Build-Logik, Metadaten, Laufzeitanforderungen, Extras, Entwicklungsumgebungen, Tool-Einstellungen, Package-Daten und Entry Points.
  2. Wähle ein Backend, das die aktuell enthaltenen Dateien, kompilierte Artefakte und Editable-Installs reproduzieren kann.
  3. Verschiebe statische, veröffentlichte Metadaten nach [project]; halte wirklich dynamische Felder explizit.
  4. Verschiebe Anforderungen, die nur Mitwirkende betreffen, nach [dependency-groups] und nicht in öffentliche Extras.
  5. Verschiebe Tool-Einstellungen nur dorthin, wo das Tool gleichwertige Semantik unterstützt.
  6. Baue sdist und Wheel, prüfe ihren Inhalt und installiere das Wheel in einer sauberen Umgebung.
  7. Führe Entry Points, Tests, Import-Prüfungen und den tatsächlichen Deployment-Pfad aus.
  8. Lösche die alte Konfiguration erst, wenn Artefakte und Verhalten übereinstimmen.

Die Schritte 6 und 7 sehen für die Beispielbibliothek so aus:

uv build                        # or: python -m build
unzip -l dist/*.whl             # files in the wheel
tar -tzf dist/*.tar.gz          # files in the sdist

uv venv /tmp/check
uv pip install --python /tmp/check dist/*.whl
/tmp/check/bin/weather --help

Vergleiche beide Dateilisten mit dem alten Build, bevor du etwas löschst. Eine fehlende Datendatei oder ein fehlendes Modul fällt hier auf, nicht im Repository-Checkout.

MANIFEST.in kann bei manchen setuptools-Layouts weiterhin nötig sein, und ein kleines setup.py kann für programmatisches Build-Verhalten gültig bleiben.

Ein aktueller uv-Workflow

uv init erzeugt je nach Flag ein anderes Layout:

# Packaged application: src/weather_app/, uv_build, and a weather-app command
uv init weather-app

# Single main.py and no build system
uv init --no-package weather-app

# Library: src layout, uv_build, and a py.typed marker
uv init --lib weather-client

uv 0.12.0 hat das Layout als Package zum Standard gemacht. Frühere Versionen erzeugten standardmäßig das Layout mit main.py.

uv init legt Projektdateien an. Die erste Projektoperation, etwa uv run, uv sync oder uv lock, erzeugt bei Bedarf das Lockfile und die dauerhafte .venv.

cd weather-client
uv add httpx
uv add --group test pytest pytest-cov
uv run pytest
uv build
uv publish

uv publish lädt die Dateien in dist/ zu PyPI hoch. Führe die Wheel-Prüfung aus dem Abschnitt zur Migration aus, bevor du veröffentlichst. Ohne uv leistet python -m build gefolgt von twine upload dist/* dasselbe.

uv legt lokale Entwicklungsanforderungen in standardisierten Dependency Groups ab. Das native Backend uv_build unterstützt nur reine Python-Projekte; kompilierte Extensions brauchen ein anderes Backend aus der Tabelle oben, etwa maturin oder scikit-build-core. Die aktuellen Backend-Einstellungen findest du in der Dokumentation zur Projektkonfiguration von uv.

Mehrere Packages in einem Repository

Ein Repository mit mehreren Packages kann sie zu einem uv-Workspace machen. Die pyproject.toml im Root listet die Mitglieder auf:

[tool.uv.workspace]
members = ["packages/*"]

Die Mitglieder teilen sich eine uv.lock und eine .venv. uv run --package <name> führt einen Befehl im Kontext eines Mitglieds aus.

Referenzen