Guide de pyproject.toml : packaging, dépendances et outils
Traduction automatique
Cet article a été traduit automatiquement depuis la version originale en anglais.
Dans les anciens projets Python, la configuration est souvent répartie entre setup.py, setup.cfg, des fichiers de requirements, MANIFEST.in et des fichiers séparés pour les outils de développement. pyproject.toml donne à ces besoins un emplacement commun, mais il ne remplace pas tous les fichiers d’un projet et toutes ses sections ne relèvent pas d’un même standard.
Ce guide s’adresse aux développeurs Python qui maintiennent un package ou une application. Vous y verrez ce que contient chacune des quatre tables principales ([build-system], [project], [dependency-groups], [tool.*]), en quoi elles diffèrent d’un lockfile et de requirements.txt, quel build backend choisir, et comment migrer un ancien projet ou en créer un nouveau avec uv.
Un fichier, quatre tables
Trois standards de packaging ont établi la structure de base :
- PEP 518 définit les exigences du système de build.
- 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 aussi réserver un espace de noms sous [tool]. Rien ne standardise son contenu : chaque outil définit ses propres clés et son propre comportement.
La figure découpe l’exemple de fichier de la section suivante en ses quatre tables et montre quel programme lit chacune. Seule [project] atteint les utilisateurs, par les métadonnées du wheel construit.
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"
Le fichier contient quatre listes de dépendances. dependencies liste ce dont le package a besoin à l’exécution, optional-dependencies ce que les utilisateurs peuvent activer en option, [dependency-groups] ce que les contributeurs installent en local, et [build-system].requires ce dont le backend a besoin pour construire.
[build-system] : comment les sources deviennent une distribution
Un build frontend tel que python -m build, pip ou uv appelle un build backend. Le backend décide comment l’arborescence source devient un sdist ou un wheel et quels fichiers entrent dans ces artefacts. Dans l’exemple, build-backend désigne hatchling, et requires liste ce que le frontend installe pour l’exécuter.
Un sdist est une archive source que l’installeur construit avant l’installation ; un wheel contient les fichiers prêts à installer.
Le build suit cet ordre. Le frontend lit [build-system], installe requires dans un environnement de build temporaire et lance le backend. Le backend lit [project] et écrit le sdist et le wheel.
requires contient les dépendances nécessaires pour exécuter le backend dans son environnement de build isolé. Ce n’est pas la liste des dépendances d’exécution de votre package.
Déclarez un système de build quand le projet produit une distribution ou que son propre code doit être installé comme package. Un projet qui n’est pas un package peut quand même utiliser pyproject.toml ; la PEP 735 autorise même un fichier ne contenant que des groupes de dépendances. Sans système de build, uv n’installe pas le projet dans son propre environnement : importer votre propre code ne fonctionne que depuis la racine du dépôt, et uv ne crée aucune commande [project.scripts]. Définissez tool.uv.package = true ou ajoutez un système de build si vous avez besoin de l’un ou de l’autre (uv project packaging).
Choisir un backend
Choisissez le backend selon ce que le build doit faire :
| Backend | À utiliser quand |
|---|---|
uv_build | Le projet est en Python pur, géré avec uv, et n’a besoin d’aucun plugin de build. uv init l’écrit. |
hatchling | Le projet est en Python pur et a besoin de plugins de build ou d’une version tirée de git (hatch-vcs). |
flit-core | Le projet est le package Python pur le plus simple et n’a besoin d’aucun plugin de build. |
setuptools | Un setup.py existant, des extensions C ou une étape de build personnalisée doivent continuer à fonctionner. |
poetry-core | L’équipe gère le projet avec Poetry. |
maturin | L’extension est écrite en Rust. |
scikit-build-core | L’extension est écrite en C ou C++ et construite avec CMake. |
meson-python | Le projet se construit avec Meson, comme NumPy et SciPy. |
La figure place les backends des builds qui ne font que copier des fichiers Python jusqu’aux builds qui compilent du code. Pour une version issue des tags Git, utilisez un backend qui le prend en charge, comme hatchling avec hatch-vcs ou setuptools avec setuptools-scm.
Copiez la table actuellement recommandée dans la documentation du backend. N’ajoutez pas wheel aux exigences de build par habitude : le backend déclare ce dont il a besoin.
[project] : les métadonnées que reçoivent les consommateurs
La table [project] décrit la distribution : son nom, sa version, sa compatibilité Python, ses dépendances d’exécution, ses points d’entrée et les autres métadonnées d’index.
Les spécificateurs de dépendance, comme httpx>=0.27 dans l’exemple, sont des contraintes pour un résolveur, pas l’instantané d’un environnement. Ils font partie des métadonnées du wheel et du sdist, de sorte que les installeurs en aval peuvent les combiner avec les exigences d’autres packages.
Les extras sont une interface d’installation publique
[project.optional-dependencies] définit des extras que les consommateurs peuvent demander. Avec l’extra cli de l’exemple, 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 et nommez-les ainsi, par exemple cli ou postgres. N’utilisez pas un extra nommé dev simplement pour regrouper tous les outils des contributeurs.
Les points d’entrée relient les commandes installées à Python
La table [project.scripts] de l’exemple a une seule entrée, weather = "weather_client.cli:main". Une fois la distribution installée, l’environnement expose une commande weather qui importe weather_client.cli et appelle main. La commande weather est installée même sans l’extra cli : cli.py doit donc fonctionner sans rich, ou indiquer à l’utilisateur d’installer weather-client[cli].
Testez la commande depuis un wheel construit, pas seulement depuis la racine du dépôt ; le wheel est ce que reçoivent les utilisateurs. La section sur la migration montre les commandes.
[dependency-groups] : des environnements locaux, non publiés
Les groupes de dépendances de la PEP 735 décrivent des environnements de développement ou sans package, sans les publier comme métadonnées du package.
[dependency-groups]
test = ["pytest>=8", "pytest-cov>=5"]
lint = ["ruff>=0.12"]
dev = [
{ include-group = "test" },
{ include-group = "lint" },
]
C’est le bon endroit pour les tests, les linters, les générateurs de documentation et les outils de contributeur du même genre. C’est aussi utile pour les applications ou les notebooks qui ne construisent pas de distribution. include-group permet à un groupe d’en réutiliser un autre ; les extras n’ont pas d’équivalent.
La PEP 735 ne définit pas d’interface en ligne de commande : chaque installeur a donc ses propres options. Les groupes et les extras s’installent ainsi :
# 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 déclare des dépendances ; il ne crée pas d’environnement. Les commandes qui en créent un et qui installent le projet avec un groupe diffèrent selon l’outil :
| Workflow | Créer l’environnement | Installer le projet et le groupe test |
|---|---|---|
| pip | python -m venv .venv, puis l’activer | pip install -e . --group test |
| uv | uv sync crée .venv s’il n’existe pas | uv sync --group test (installe aussi le projet et dev) |
[tool.*] : la configuration propre à un outil
Les tables d’outils ne partagent pas de schéma. Dans l’exemple, [tool.pytest.ini_options] et [tool.ruff] utilisent chacune des clés que seul leur outil lit. N’utilisez une table [tool.*] que si l’outil la documente. Ces outils courants lisent leurs paramètres dans pyproject.toml :
| Tâche | Outil | Table |
|---|---|---|
| Formatage et linting | Ruff | [tool.ruff] |
| Tests | pytest | [tool.pytest.ini_options] ou [tool.pytest] |
| Vérification de types | mypy | [tool.mypy] |
| Couverture | coverage.py | [tool.coverage.run], [tool.coverage.report] |
pytest 9.0 a ajouté [tool.pytest], qui utilise des types TOML natifs, comme une liste pour addopts ; les versions plus anciennes ne lisent que [tool.pytest.ini_options].
Certains paramètres restent dans leurs propres fichiers, parce qu’un autre programme les lit ou que le fichier demande un autre format. flake8 ne lit pas pyproject.toml ; il lit .flake8, setup.cfg ou tox.ini. pre-commit lit .pre-commit-config.yaml.
Déclarations, locks et fichiers requirements résolvent des problèmes différents
La figure suit les packages de l’exemple de fichier. Chaque liste atteint un environnement différent, et les utilisateurs ne reçoivent que les deux listes de [project].
Cinq endroits contiennent des informations sur les dépendances. Chacun a un seul rôle :
| Artefact | Objectif principal | Contenu typique |
|---|---|---|
[project.dependencies] | Contrat d’exécution publié | Exigences directes et plages compatibles |
[project.optional-dependencies] | Fonctionnalités optionnelles publiées | Extras destinés aux consommateurs |
[dependency-groups] | Environnements locaux non publiés | Groupes de test, de lint, de documentation ou d’application |
| Lockfile | Reproduire un environnement résolu | Versions exactes, sources et métadonnées de résolution |
requirements.txt | Entrée d’installation compatible avec pip | Requirements plus options, contraintes, URL ou hashes propres à pip |
Une bibliothèque publie normalement des contraintes compatibles et se teste sur une plage de versions. Une application commite en général le lockfile du gestionnaire d’environnement. Les versions exactes appartiennent à l’artefact de déploiement résolu, pas aux métadonnées publiques d’une bibliothèque, où elles seraient figées sans réflexion. Par exemple, une bibliothèque déclare httpx>=0.27 et le résolveur de l’utilisateur choisit la version. Une application commite uv.lock et l’installe avec uv sync --locked.
Gardez un fichier requirements quand 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 jeux 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
Ne commencez pas par supprimer setup.py ou requirements.txt. Classez d’abord le rôle de chaque fichier existant. La figure montre où va chaque partie d’un ancien setup dans pyproject.toml, et quels fichiers restent.
- Faites l’inventaire de la logique de build, des métadonnées, des dépendances d’exécution, des extras, des environnements de développement, des paramètres d’outils, des données du package et des points d’entrée.
- Choisissez un backend capable de reproduire l’inclusion actuelle des fichiers, les artefacts compilés et les installations en mode éditable.
- Déplacez les métadonnées publiées statiques dans
[project]; gardez explicites les champs réellement dynamiques. - Déplacez les exigences réservées aux contributeurs dans
[dependency-groups], pas dans des extras publics. - Déplacez les paramètres d’outils seulement là où l’outil gère une sémantique équivalente.
- Construisez un sdist et un wheel, inspectez leur contenu et installez le wheel dans un environnement propre.
- Exécutez les points d’entrée, les tests, les vérifications d’import et le vrai chemin de déploiement.
- Supprimez l’ancienne configuration seulement quand les artefacts et le comportement correspondent.
Les étapes 6 et 7 ressemblent à ceci pour la bibliothèque de l’exemple :
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
Comparez les deux listes de fichiers avec l’ancien build avant de supprimer quoi que ce soit. Un fichier de données ou un module manquant apparaît ici, pas dans le checkout du dépôt.
MANIFEST.in peut rester nécessaire avec certaines organisations setuptools, et un petit setup.py peut rester valide pour un comportement de build programmatique.
Un workflow uv actuel
uv init crée une structure différente selon l’option. Choisissez l’une de ces alternatives :
# 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 a fait de la structure packagée le choix par défaut. Les versions antérieures créaient par défaut la structure avec main.py.
uv init crée les fichiers du projet. uv lock crée ou met à jour uv.lock sans créer .venv. uv sync ou uv run met à jour le lockfile et crée et synchronise .venv selon les besoins.
Continuez avec la bibliothèque créée par uv init --lib weather-client. Elle contient une fonction hello() générée : ajoutez donc un petit test avant d’exécuter pytest :
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
Le test devrait afficher 1 passed. --group test sélectionne les dépendances de test même après leur suppression par un uv sync avec les options par défaut.
Inspectez les archives et installez le wheel dans un environnement propre avant de publier, comme indiqué dans la section sur la migration. Cette bibliothèque générée n’a pas de commande weather : vérifiez plutôt import weather_client. Une fois prêt, envoyez les fichiers de dist/ vers PyPI :
uv publish
En dehors d’uv, python -m build suivi de twine upload dist/* fait la même chose.
uv place les dépendances de développement locales dans des groupes de dépendances standardisés. Son backend natif uv_build ne gère que les projets en Python pur ; les extensions compilées demandent un autre backend du tableau ci-dessus, comme maturin ou scikit-build-core. Consultez la documentation de configuration des projets d’uv pour les paramètres actuels des backends.
Plusieurs packages dans un même dépôt
Un dépôt contenant plusieurs packages peut en faire un workspace uv. Le pyproject.toml racine liste les membres :
[tool.uv.workspace]
members = ["packages/*"]
Les membres partagent un seul uv.lock et un seul .venv. uv run --package <name> exécute une commande dans le contexte d’un seul membre.