pyproject.toml-Leitfaden: Packaging, Dependencies und Tools

Automatische Übersetzung Dieser Artikel wurde automatisch aus der englischen Originalversion übersetzt.

Ältere Python-Projekte verteilen ihre Konfiguration häufig auf setup.py, setup.cfg, Requirements-Dateien, MANIFEST.in und separate Dateien für Development-Tools. pyproject.toml bietet diesen Belangen einen gemeinsamen Ort, ersetzt jedoch nicht jede Projektdatei und macht auch nicht jeden Abschnitt zu einem Teil eines einzigen Standards.

Dieser Leitfaden führt von der Build-Konfiguration über Projektmetadaten bis zu Tool-Einstellungen durch die Datei. Vier Verantwortliche halten die Abschnitte getrennt: Was das Projekt baut, was das Projekt veröffentlicht, was Contributors lokal benötigen und welche Konfiguration die einzelnen Tools verwalten.

TL;DR. Verwende [build-system] für das Backend, das eine Distribution baut, [project] für veröffentlichte Metadaten und Runtime-Anforderungen, [dependency-groups] für unveröffentlichte Development-Environments und [tool.*] nur dort, wo die Dokumentation des jeweiligen Tools dies vorsieht. Eine Dependency-Deklaration ist kein Lockfile.

Eine Datei, vier Verantwortliche

Die vier Verantwortungsgrenzen in pyproject.tomlDie vier Verantwortungsgrenzen in pyproject.toml

Drei Packaging-Standards bilden die grundlegende Struktur:

  • PEP 518 definiert Build-System-Anforderungen.
  • PEP 621 definiert Projektmetadaten.
  • PEP 735 definiert unveröffentlichte Dependency Groups.

Tool-Autoren können außerdem einen Namespace unter [tool] beanspruchen. Was dort hineingehört, ist nicht standardisiert: Jedes Tool definiert seine eigenen Keys und sein eigenes Verhalten.

Hier ist eine kleine paketierte Library:

[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"

Jede Dependency-Liste beantwortet eine andere Frage.

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

Ein Build-Frontend wie python -m build, pip oder uv ruft ein Build-Backend auf. Das Backend entscheidet, wie aus dem Source Tree ein sdist oder Wheel wird und welche Dateien in diese Artefakte aufgenommen werden.

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

requires enthält Dependencies, die zum Ausführen des Backends in seiner isolierten Build-Environment benötigt werden. Es ist nicht die Runtime-Dependency-Liste deines Packages.

Deklariere ein Build-System, wenn das Projekt eine Distribution erzeugt oder eigenen Code als Package installieren muss. Auch ein Non-Package-Projekt kann pyproject.toml verwenden; PEP 735 erlaubt sogar eine Datei, die ausschließlich Dependency Groups enthält. Environment Manager unterscheiden sich darin, wie sie ein Projekt ohne Build-System behandeln. Triff diese Entscheidung daher bewusst.

Wähle das Backend anhand der Build-Anforderungen:

  • Layout und Dateiauswahl für reines Python
  • kompilierte Extensions oder externe Build-Systeme
  • Anforderungen an dynamische Versionen oder generierte Dateien
  • Verhalten bei Editable Installs
  • Reifegrad des Backends in der Release-Pipeline

Übernimm die aktuell empfohlene Tabelle des Backends aus dessen Dokumentation. Füge wheel nicht aus Gewohnheit zu den Build-Anforderungen hinzu; das Backend definiert, was es benötigt.

[project]: Metadaten für Consumer

Die Tabelle [project] beschreibt die Distribution: ihren Namen, ihre Version, Python-Kompatibilität, Runtime-Dependencies, Entry Points und weitere Index-Metadaten.

[project]
name = "weather-client"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = ["httpx>=0.27"]

Die Dependency-Specifier sind Constraints für einen Resolver, kein Snapshot einer einzelnen Environment. Sie werden Teil der Wheel- und sdist-Metadaten, sodass nachgelagerte Installer sie mit Requirements anderer Packages kombinieren können.

Extras sind eine öffentliche Installationsschnittstelle

[project.optional-dependencies] definiert Extras, die Consumer anfordern können:

[project.optional-dependencies]
cli = ["rich>=13"]
postgres = ["psycopg[binary]>=3.2"]

Ein Consumer kann weather-client[cli] installieren. Da Extra-Namen und Requirements veröffentlicht werden, solltest du sie als Produktfähigkeiten behandeln. Verwende kein Extra namens dev lediglich als Container für alle Contributor-Tools.

Entry Points verbinden installierte Commands mit Python

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

Nach der Installation der Distribution stellt die Environment weather bereit. Dieser Command importiert weather_client.cli:main und ruft es auf. Teste den Command mit einem gebauten Wheel und nicht nur aus dem Repository-Root; das Wheel ist das Artefakt, das User erhalten.

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

PEP-735-Dependency-Groups beschreiben Development- oder Non-Package-Environments, 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" },
]

Das ist die richtige Grenze für Tests, Linter, Dokumentations-Builder und ähnliche Contributor-Tools. Dependency Groups sind auch für Applications oder Notebooks nützlich, die keine Distribution bauen.

Dependency Groups sind standardisierte Daten, aber die Installer-Schnittstellen unterscheiden sich weiterhin. Prüfe, wie der ausgewählte Environment Manager sie installiert, lockt und auflöst. PEP 735 definiert keine universelle Command-Line-Schnittstelle.

[tool.*]: Konfiguration im Besitz eines einzelnen Tools

Tool-Tabellen verwenden kein gemeinsames Schema:

[tool.pytest.ini_options]
addopts = "-ra"
testpaths = ["tests"]

[tool.ruff]
line-length = 100

Verwende eine [tool.*]-Tabelle nur, wenn das Tool sie dokumentiert. Einige Einstellungen gehören weiterhin in eigene Dateien, weil ein anderes Ecosystem sie konsumiert, die Datei ein anderes Format benötigt oder die Konfiguration eigenständig übersichtlicher ist. pyproject.toml ist ein Koordinationspunkt. Daraus folgt nicht, dass jede Einstellung zentralisiert werden muss.

Configuration Consumers rund um pyproject.tomlConfiguration Consumers rund um pyproject.toml

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

Eine häufige Ursache für Verwirrung ist, jedes Dependency-Artefakt als konkurrierende Source of Truth zu betrachten.

ArtefaktHauptzweckTypische Inhalte
[project.dependencies]Öffentlicher Runtime-VertragDirekte Requirements und kompatible Bereiche
[project.optional-dependencies]Veröffentliche Opt-in-FähigkeitenConsumer-facing Extras
[dependency-groups]Unveröffentlichte lokale EnvironmentsTest-, Lint-, Docs- oder Application-Groups
LockfileReproduzierbare aufgelöste EnvironmentExakte Versionen, Quellen und Resolution-Metadaten
requirements.txtpip-kompatibler Installations-InputRequirements plus pip-spezifische Optionen, Constraints, URLs oder Hashes

Eine Library veröffentlicht normalerweise kompatible Constraints und testet gegen einen Versionsbereich. Eine Application committed üblicherweise das Lockfile des Environment Managers. Exakte Pins gehören in das aufgelöste Deployment-Artefakt und nicht blind in die öffentlichen Metadaten einer Library.

Behalte eine Requirements-Datei, wenn eine Integration das pip-Format oder dessen Features benötigt. Wenn ein uv-managed Project eine solche Datei benötigt, exportiere sie aus dem Lockfile, statt zwei unabhängige Dependency-Sets zu pflegen:

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

Der Export ist abgeleiteter Kompatibilitäts-Output. Das Lockfile bleibt für diesen Workflow die aufgelöste Source of Truth.

Eine Migration, die das Verhalten bewahrt

Eine verifikationsorientierte Migration zu pyproject.tomlEine verifikationsorientierte Migration zu pyproject.toml

Beginne nicht damit, setup.py oder requirements.txt zu löschen. Klassifiziere zuerst, welche Aufgabe jede bestehende Datei erfüllt.

  1. Erfasse Build-Logik, Metadaten, Runtime-Requirements, Extras, Developer-Environments, Tool-Einstellungen, Package-Daten und Entry Points.
  2. Wähle ein Backend, das die aktuelle Dateiauswahl, kompilierte Artefakte und Editable Installs reproduzieren kann.
  3. Verschiebe statische veröffentlichte Metadaten nach [project]; halte tatsächlich dynamische Felder explizit.
  4. Verschiebe ausschließlich für Contributors bestimmte Requirements nach [dependency-groups] und nicht in öffentliche Extras.
  5. Verschiebe Tool-Einstellungen nur dann, wenn das Tool gleichwertige Semantik unterstützt.
  6. Baue sowohl ein sdist als auch ein Wheel, prüfe deren Inhalte und installiere das Wheel in einer sauberen Environment.
  7. Führe Entry Points, Tests, Import-Checks und den tatsächlichen Deployment-Pfad aus.
  8. Lösche alte Konfiguration erst, nachdem Artefakte und Verhalten übereinstimmen.

MANIFEST.in kann bei einigen setuptools-Layouts weiterhin erforderlich sein, und ein kleines setup.py kann für programmatisches Build-Verhalten weiterhin sinnvoll sein. Modernisierung verschiebt Verantwortlichkeiten zwischen Dateien. Das Löschen von Dateien ist nicht das Ziel.

Ein aktueller uv-Workflow

uv unterscheidet beim Erstellen eines Projects zwischen Applications und Libraries:

# Non-library application template
uv init weather-app

# Packaged library with a src layout and build system
uv init --lib weather-client

uv init erstellt Projektdateien. Die erste Project-Operation, etwa uv run, uv sync oder uv lock, erstellt bei Bedarf das Lockfile und .venv dauerhaft.

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

uv legt lokale Development-Requirements derzeit in standardisierten Dependency Groups ab. Sein natives uv_build-Backend ist eine Option für reine Python-Projekte; kompilierte Extensions benötigen eine geeignete Alternative wie maturin oder scikit-build-core. Die aktuellen Backend-Einstellungen findest du in der Project-Configuration-Dokumentation von uv.

Fazit

pyproject.toml ist übersichtlich, wenn jede Tabelle genau eine Zielgruppe hat. Build-Isolation gehört zu [build-system] und veröffentlichtes Verhalten zu [project]. Contributor-Environments gehören zu [dependency-groups]. Tool-Schalter gehören zu dem Tool, das sie definiert.

Sobald diese Grenzen stabil sind, lässt sich die Datei leichter reviewen, und Migrationen verwechseln Package-Metadaten nicht mehr mit der aufgelösten Environment einer einzelnen Maschine.

Referenzen