Guide de pyproject.toml : packaging, dépendances et outils

Traduction automatique Cet article a été traduit automatiquement depuis la version originale en anglais.

Les anciens projets Python répartissent souvent leur configuration entre setup.py, setup.cfg, des fichiers de requirements, MANIFEST.in et des fichiers distincts pour les outils de développement. pyproject.toml offre un emplacement commun à ces préoccupations, sans toutefois remplacer tous les fichiers du projet ni faire de chaque section une partie d’un standard unique.

Ce guide parcourt le fichier, de la configuration du build aux métadonnées du projet et aux paramètres des outils. Quatre responsables permettent de conserver des sections distinctes : ce qui construit le projet, ce que le projet publie, ce dont les contributeurs ont besoin en local et ce que configurent les outils eux-mêmes.

TL;DR. Utilisez [build-system] pour le backend qui construit une distribution, [project] pour les métadonnées publiées et les exigences d’exécution, [dependency-groups] pour les environnements de développement non publiés, et [tool.*] uniquement lorsque la documentation de l’outil l’indique. Une déclaration de dépendances n’est pas un lockfile.

Un fichier, quatre responsables

Les quatre périmètres de responsabilité dans pyproject.tomlLes quatre périmètres de responsabilité dans pyproject.toml

Trois standards de packaging ont établi la structure centrale :

  • PEP 518 définit les exigences du build system.
  • PEP 621 définit les métadonnées du projet.
  • PEP 735 définit les groupes de dépendances non publiés.

Les auteurs d’outils peuvent également revendiquer un espace de noms sous [tool]. Rien ne standardise ce qui s’y trouve : chaque outil définit ses propres clés et son propre comportement.

Voici une petite bibliothèque packagée :

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

Chaque liste de dépendances répond à une question différente.

[build-system] : comment le code source devient une distribution

Un build frontend tel que python -m build, pip ou uv invoque un build backend. Le backend détermine comment l’arborescence source devient un sdist ou un wheel, ainsi que les fichiers inclus dans ces artefacts.

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

requires contient les dépendances nécessaires à l’exécution du backend dans son environnement de build isolé. Il ne s’agit pas de la liste des dépendances d’exécution de votre package.

Déclarez un build system lorsque le projet produit une distribution ou doit installer son propre code comme package. Un projet qui n’est pas un package peut tout de même utiliser pyproject.toml ; PEP 735 autorise même un fichier contenant uniquement des groupes de dépendances. Les gestionnaires d’environnements ne traitent pas tous de la même façon un projet sans build system : faites donc ce choix délibérément.

Choisissez le backend en fonction des exigences du build :

  • layout purement Python et besoins de sélection des fichiers ;
  • extensions compilées ou systèmes de build externes ;
  • version dynamique ou génération de fichiers ;
  • comportement des installations éditables ;
  • maturité du backend dans le pipeline de release.

Reprenez la table recommandée à jour du backend dans sa documentation. N’ajoutez pas wheel aux exigences du build par habitude ; c’est le backend qui déclare ce dont il a besoin.

[project] : les métadonnées reçues par les consommateurs

La table [project] décrit la distribution : son nom, sa version, sa compatibilité avec Python, ses dépendances d’exécution, ses points d’entrée et les autres métadonnées destinées aux index.

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

Les specifiers de dépendances sont des contraintes destinées à un resolver, et non un instantané d’un environnement donné. Ils font partie des métadonnées du wheel et du sdist, ce qui permet aux installateurs en aval de les combiner avec les exigences d’autres packages.

Les extras constituent une interface d’installation publique

[project.optional-dependencies] définit les extras que les consommateurs peuvent demander :

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

Un consommateur peut installer weather-client[cli]. Comme les noms des extras et leurs exigences sont publiés, traitez-les comme des fonctionnalités du produit. N’utilisez pas un extra nommé dev uniquement pour regrouper tous les outils des contributeurs.

Les points d’entrée relient les commandes installées à Python

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

Une fois la distribution installée, l’environnement expose weather, qui importe et appelle weather_client.cli:main. Testez la commande à partir d’un wheel construit, et pas seulement depuis la racine du dépôt ; c’est le wheel que reçoivent les utilisateurs.

[dependency-groups] : des environnements locaux non publiés

Les groupes de dépendances PEP 735 décrivent les environnements de développement ou les environnements qui ne sont pas des packages, sans les publier sous forme de métadonnées de package.

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

C’est la frontière appropriée pour les tests, les linters, les générateurs de documentation et autres outils destinés aux contributeurs. C’est également utile pour les applications ou les notebooks qui ne construisent pas de distribution.

Les groupes de dépendances constituent des données standardisées, mais les interfaces des installateurs varient encore. Vérifiez comment le gestionnaire d’environnements choisi les installe, les verrouille et les résout. PEP 735 ne définit pas d’interface en ligne de commande universelle.

[tool.*] : la configuration détenue par un seul outil

Les tables des outils ne partagent pas de schéma :

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

[tool.ruff]
line-length = 100

Utilisez une table [tool.*] uniquement si l’outil la documente. Certains paramètres doivent toujours rester dans leurs propres fichiers parce qu’un autre écosystème les consomme, parce que le fichier nécessite un format différent ou parce que la configuration y est plus lisible. pyproject.toml est un point de coordination. Il n’impose pas de centraliser tous les paramètres.

Les consommateurs de configuration autour de pyproject.tomlLes consommateurs de configuration autour de pyproject.toml

Les déclarations, les locks et les fichiers de requirements répondent à des problèmes différents

Une confusion fréquente consiste à considérer chaque artefact de dépendances comme une source de vérité concurrente.

ArtefactObjectif principalContenu typique
[project.dependencies]Contrat d’exécution publiéExigences directes et plages compatibles
[project.optional-dependencies]Fonctionnalités publiées et optionnellesExtras destinés aux consommateurs
[dependency-groups]Environnements locaux non publiésGroupes de test, lint, documentation ou application
LockfileReproduire un environnement résoluVersions exactes, sources et métadonnées de résolution
requirements.txtEntrée d’installation compatible avec pipRequirements et options propres à pip, contraintes, URLs ou hashes

Une bibliothèque publie généralement des contraintes compatibles et effectue ses tests sur une plage de versions. Une application versionne habituellement le lockfile du gestionnaire d’environnements. Les pins exacts doivent figurer dans l’artefact de déploiement résolu, et non être ajoutés aveuglément aux métadonnées publiques d’une bibliothèque.

Conservez un fichier de requirements lorsqu’une intégration exige le format ou les fonctionnalités de pip. Si un projet géré par uv en a besoin, exportez-le depuis le lock plutôt que de maintenir deux ensembles de dépendances indépendants :

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

L’export est une sortie de compatibilité dérivée. Le lock reste la source résolue pour ce workflow.

Une migration qui préserve le comportement

Une migration vers pyproject.toml fondée sur la vérificationUne migration vers pyproject.toml fondée sur la vérification

Ne commencez pas par supprimer setup.py ou requirements.txt. Commencez par classer le rôle de chaque fichier existant.

  1. Inventoriez la logique de build, les métadonnées, les exigences d’exécution, les extras, les environnements des développeurs, les paramètres des outils, les données du package et les points d’entrée.
  2. Sélectionnez un backend capable de reproduire l’inclusion actuelle des fichiers, les artefacts compilés et les installations éditables.
  3. Déplacez les métadonnées statiques publiées dans [project] ; laissez les champs réellement dynamiques explicitement déclarés.
  4. Déplacez les exigences réservées aux contributeurs dans [dependency-groups], et non dans les extras publics.
  5. Déplacez les paramètres des outils uniquement lorsque l’outil prend en charge des sémantiques équivalentes.
  6. Construisez un sdist et un wheel, inspectez leur contenu et installez le wheel dans un environnement propre.
  7. Exécutez les points d’entrée, les tests, les vérifications d’import et le chemin de déploiement réel.
  8. Supprimez l’ancienne configuration uniquement lorsque les artefacts et le comportement correspondent.

MANIFEST.in peut encore être nécessaire avec certains layouts setuptools, et un petit setup.py peut rester pertinent pour un comportement de build programmatique. La modernisation déplace la responsabilité entre les fichiers. Supprimer des fichiers n’est pas l’objectif.

Un workflow uv actuel

uv distingue les applications des bibliothèques lors de la création d’un projet :

# Non-library application template
uv init weather-app

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

uv init crée les fichiers du projet. La première opération sur le projet, comme uv run, uv sync ou uv lock, crée le lockfile et le .venv persistant si nécessaire.

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

uv place actuellement les exigences de développement local dans des groupes de dépendances standardisés. Son backend uv_build natif constitue une option pour les projets purement Python ; les extensions compilées nécessitent une alternative adaptée, comme maturin ou scikit-build-core. Consultez la documentation de configuration des projets de uv pour connaître les paramètres actuels du backend.

Conclusion

pyproject.toml est clair lorsque chaque table a un seul public. L’isolation du build appartient à [build-system] et le comportement publié à [project]. Les environnements des contributeurs appartiennent à [dependency-groups]. Les options des outils appartiennent à l’outil qui les définit.

Une fois ces frontières stabilisées, le fichier devient plus facile à relire, et les migrations cessent de confondre les métadonnées du package avec l’environnement résolu d’une seule machine.

Références