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 teilt die Beispieldatei aus dem nächsten Abschnitt in ihre vier Tabellen und zeigt, welches Programm welche liest. Nur [project] erreicht die Nutzer, und zwar über die Metadaten im gebauten Wheel.

Die Beispieldatei pyproject.toml, aufgeteilt in ihre vier Tabellen: Das Build-Frontend liest build-system, hatchling macht aus project die Wheel-Metadaten, die pip für den Nutzer installiert, uv sync installiert die Gruppe test in .venv, und pytest und ruff lesen ihre eigenen TabellenDie Beispieldatei pyproject.toml, aufgeteilt in ihre vier Tabellen: Das Build-Frontend liest build-system, hatchling macht aus project die Wheel-Metadaten, die pip für den Nutzer installiert, uv sync installiert die Gruppe test in .venv, und pytest und ruff lesen ihre eigenen Tabellen

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 sdist ist ein Quellarchiv, das ein Installer vor der Installation baut; ein Wheel enthält die Dateien bereits in installierbarer Form.

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 ordnet die Backends von Builds, die nur Python-Dateien kopieren, bis zu Builds, die Code kompilieren. Für eine Version aus Git-Tags nutze ein Backend mit Unterstützung für Git-Versionen, etwa hatchling mit hatch-vcs oder setuptools mit setuptools-scm.

Build-Backends in vier Spalten: reines Python ohne Plugins (uv_build, flit-core), reines Python mit Plugins (hatchling), ein alter Build über setup.py (setuptools) und kompilierte Erweiterungen (maturin, scikit-build-core, meson-python)Build-Backends in vier Spalten: reines Python ohne Plugins (uv_build, flit-core), reines Python mit Plugins (hatchling), ein alter Build über setup.py (setuptools) und kompilierte Erweiterungen (maturin, scikit-build-core, meson-python)

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

Die Abbildung folgt den Packages aus der Beispieldatei. Jede Liste erreicht eine andere Umgebung, und Nutzer erhalten nur die beiden [project]-Listen.

Ein Raster aus den vier Abhängigkeitslisten und drei Umgebungen: hatchling nur in der temporären Build-Umgebung, httpx für Nutzer und Contributor, rich nur für Nutzer mit dem Extra cli, und pytest und pytest-cov nur in der .venv der ContributorEin Raster aus den vier Abhängigkeitslisten und drei Umgebungen: hatchling nur in der temporären Build-Umgebung, httpx für Nutzer und Contributor, rich nur für Nutzer mit dem Extra cli, und pytest und pytest-cov nur in der .venv der Contributor

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, wohin jeder Teil eines alten Setups in pyproject.toml wandert und welche Dateien bleiben.

Alte Dateien und ihre neuen Orte: setup.py-Metadaten, install_requires, extras_require und Entry Points in project-Tabellen, Dev-Requirements in dependency-groups, pytest.ini in eine tool-Tabelle, während ein kleines setup.py, MANIFEST.in, .flake8 und eine aus uv.lock exportierte requirements.txt außerhalb bleibenAlte Dateien und ihre neuen Orte: setup.py-Metadaten, install_requires, extras_require und Entry Points in project-Tabellen, Dev-Requirements in dependency-groups, pytest.ini in eine tool-Tabelle, während ein kleines setup.py, MANIFEST.in, .flake8 und eine aus uv.lock exportierte requirements.txt außerhalb bleiben

  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. Wähle eine dieser Alternativen:

# 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. uv lock erstellt oder aktualisiert uv.lock, ohne .venv anzulegen. uv sync oder uv run aktualisiert das Lockfile und erstellt und synchronisiert .venv bei Bedarf.

Mache mit der Bibliothek weiter, die uv init --lib weather-client erstellt hat. Sie enthält eine generierte Funktion hello(). Füge deshalb einen kleinen Test hinzu, bevor du pytest ausführst:

cd weather-client
uv add httpx
uv add --group test pytest pytest-cov
mkdir -p tests
cat > tests/test_weather_client.py <<'PY'
from weather_client import hello

def test_hello():
    assert hello() == "Hello from weather-client!"
PY
uv run --group test pytest
uv build

Der Test sollte 1 passed melden. --group test wählt die Testabhängigkeiten auch dann aus, wenn ein standardmäßiges uv sync sie entfernt hat.

Prüfe die Archive und installiere das Wheel vor der Veröffentlichung in einer sauberen Umgebung, wie im Abschnitt zur Migration gezeigt. Diese generierte Bibliothek hat keinen Befehl weather. Prüfe stattdessen import weather_client. Wenn alles bereit ist, lade die Dateien in dist/ zu PyPI hoch:

uv publish

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