Développement Web

Packaging Python avec pyproject.toml en 2026 : build et publication PyPI

10 min de lecture

Packager une bibliothèque ou une application Python en 2026 ne devrait plus avoir à passer par les vieux setup.py, setup.cfg, MANIFEST.in. La PEP 621 a unifié les métadonnées de projet dans pyproject.toml ; la PEP 517/518 a standardisé les build backends ; et l’écosystème (uv, Hatch, setuptools, Poetry) s’est aligné. Le résultat : un seul fichier de configuration humainement lisible, déterministe, compatible PyPI, et exploitable par tous les outils. Ce tutoriel construit pas à pas un projet packageable : structure src-layout, métadonnées PEP 621, build backend, build local, publication sur PyPI, et versioning automatique.

Prérequis

  • Python 3.13.13 ou 3.14.5 disponible
  • uv 0.11+ installé (cf. Ruff et uv workflow) ou pip 24+
  • Compte PyPI et compte TestPyPI (gratuit) si vous comptez publier
  • Temps estimé : 90 minutes

Étape 1 — Adopter le src-layout

Le src-layout est la structure de projet recommandée par la Python Packaging Authority depuis 2020. Elle place le code source sous un dossier src/ au lieu de la racine, ce qui évite que python -m pytest trouve accidentellement le package non installé via le sys.path implicite. Cette discipline détecte tôt les imports manquants.

mon-package/
├── pyproject.toml
├── README.md
├── LICENSE
├── CHANGELOG.md
├── .gitignore
├── .python-version
├── src/
│   └── mon_package/
│       ├── __init__.py
│       ├── core.py
│       ├── cli.py
│       └── py.typed         # signal PEP 561 : package typé
└── tests/
    ├── __init__.py
    ├── conftest.py
    └── test_core.py

Le fichier py.typed (vide) signale aux outils de type-checking que le package distribue ses propres annotations de type — un standard PEP 561 indispensable pour que mypy ou pyright respectent vos types côté consommateur. Le __init__.py peut être vide ou ré-exporter l’API publique (from .core import Client). Les fichiers __init__.py sous tests/ servent à exécuter pytest depuis n’importe quel sous-dossier sans casser les imports relatifs.

Étape 2 — pyproject.toml minimal PEP 621

La table [project] standardisée par la PEP 621 contient toutes les métadonnées qu’on déclarait historiquement dans setup.py. Les champs essentiels : name, version, description, readme, requires-python, license, authors, dependencies.

[project]
name = "itsc-tools"
version = "0.3.2"
description = "Outils de productivité pour développeurs francophones."
readme = "README.md"
requires-python = ">=3.13"
license = { text = "MIT" }
authors = [
    { name = "Mamadou Diallo", email = "contact@itskillscenter.io" }
]
keywords = ["productivity", "cli", "dev-tools"]
classifiers = [
    "Development Status :: 4 - Beta",
    "Intended Audience :: Developers",
    "License :: OSI Approved :: MIT License",
    "Operating System :: OS Independent",
    "Programming Language :: Python :: 3",
    "Programming Language :: Python :: 3.13",
    "Programming Language :: Python :: 3.14",
    "Topic :: Software Development :: Libraries",
    "Typing :: Typed"
]
dependencies = [
    "httpx>=0.27,<1.0",
    "pydantic>=2.13,<3.0",
    "click>=8.1",
]

[project.urls]
Homepage = "https://itskillscenter.io"
Documentation = "https://docs.itskillscenter.io/tools"
Repository = "https://github.com/itskillscenter/tools"
Issues = "https://github.com/itskillscenter/tools/issues"
Changelog = "https://github.com/itskillscenter/tools/blob/main/CHANGELOG.md"

Trois éléments critiques. Les classifiers alimentent les filtres de recherche PyPI — choisissez-les soigneusement, la liste officielle est sur pypi.org/classifiers. Typing :: Typed indique au monde que le package est typé. project.urls alimente les liens dans la page PyPI : sans Repository, les visiteurs ne trouvent pas votre code source.

Étape 3 — Choisir un build backend

Le build backend est le programme qui transforme votre code source en wheel et sdist. Quatre options dominantes en 2026 : setuptools (historique, le plus connu), hatchling (moderne, par Hatch), flit-core (minimaliste), uv build (intégré uv). Pour 90 % des projets, hatchling est le bon choix : configuration zéro, gestion VCS automatique, support des plugins.

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

[tool.hatch.version]
path = "src/itsc_tools/__init__.py"  # lit __version__ depuis le code

[tool.hatch.build.targets.wheel]
packages = ["src/itsc_tools"]

[tool.hatch.build.targets.sdist]
include = [
    "/src",
    "/tests",
    "/README.md",
    "/LICENSE",
    "/CHANGELOG.md"
]

Le hatch.version.path évite de dupliquer la version : on l’écrit une seule fois dans __init__.py sous __version__ = "0.3.2", et hatchling lit cette valeur à la build. Vous pouvez aussi utiliser fallback-version et hatch-vcs pour dériver la version directement depuis les tags Git. Le sdist.include contrôle ce qui entre dans la tarball source ; par défaut, hatchling inclut tout sauf les patterns du .gitignore.

Étape 4 — Entry points et scripts CLI

Pour qu’un utilisateur installant votre package puisse lancer itsc en ligne de commande, on déclare un entry point dans pyproject.toml. Le standard PEP 621 supporte project.scripts (CLI standard), project.gui-scripts (apps GUI sans terminal sur Windows), et project.entry-points (plugins discoverables).

[project.scripts]
itsc = "itsc_tools.cli:main"
itsc-format = "itsc_tools.formatters:cli_entry"

[project.gui-scripts]
itsc-dashboard = "itsc_tools.dashboard:main"

# Plugin discoverable par d'autres packages
[project.entry-points."pytest11"]
itsc_plugin = "itsc_tools.pytest_plugin"

Une fois le package installé via pip install itsc-tools ou uv add itsc-tools, la commande itsc devient disponible dans le PATH du venv. La fonction pointée doit exister et être appelable sans arguments — typiquement un dispatcher Click ou argparse. L’entry point pytest11 est un exemple de plugin : pytest le découvre automatiquement à l’installation.

Étape 5 — Builder le package localement

Avant publication, il faut tester la build localement et inspecter le résultat. uv expose uv build qui invoque le backend déclaré et produit la wheel + le sdist dans dist/.

# Build avec uv
uv build

# Résultat dans dist/
ls dist/
# itsc_tools-0.3.2-py3-none-any.whl
# itsc_tools-0.3.2.tar.gz

# Inspecter le contenu de la wheel
unzip -l dist/itsc_tools-0.3.2-py3-none-any.whl

# Inspecter le sdist
tar -tzf dist/itsc_tools-0.3.2.tar.gz

# Tester l'installation dans un venv temporaire
uv venv .test-install
.test-install/bin/python -m pip install dist/itsc_tools-0.3.2-py3-none-any.whl
.test-install/bin/itsc --help

# Avec twine pour vérifier les métadonnées
uv tool run twine check dist/*

L’étape twine check valide la conformité PyPI : README rendu correctement (Markdown ou reStructuredText), métadonnées présentes, classifiers valides. Un twine check en erreur fait échouer l’upload, autant le détecter en local. Le test d’installation dans un venv jetable confirme que la wheel est auto-suffisante (pas de fichier oublié, pas d’import qui casse).

Étape 6 — Versioning et CHANGELOG

Le versioning sémantique (SemVer) reste le standard : MAJOR.MINOR.PATCH. Pour automatiser la propagation entre __init__.py, CHANGELOG.md, et le tag Git, deux approches courantes : commitizen (génère le CHANGELOG depuis les commits conventionnels) ou hatch-vcs (lit la version directement depuis les tags Git).

# Approche hatch-vcs : version dérivée du tag Git
[build-system]
requires = ["hatchling", "hatch-vcs"]
build-backend = "hatchling.build"

[tool.hatch.version]
source = "vcs"

[tool.hatch.build.hooks.vcs]
version-file = "src/itsc_tools/_version.py"

# Approche commitizen : config dans pyproject.toml
[tool.commitizen]
name = "cz_conventional_commits"
version = "0.3.2"
version_files = [
    "src/itsc_tools/__init__.py:__version__",
    "pyproject.toml:version"
]
tag_format = "v$version"
update_changelog_on_bump = true

# Workflow commitizen
uv tool install commitizen
cz commit            # commit guidé conventionnel
cz bump              # incrémente version + tag + changelog

L’approche commitizen est plus structurée pour les équipes : les commits doivent respecter conventional commits (feat:, fix:, BREAKING CHANGE:), et la version est calculée automatiquement selon les types de commits depuis le dernier tag. Pour un projet personnel, hatch-vcs est plus léger : un git tag v0.4.0 suffit, et le numéro propagé dans la wheel.

Étape 7 — Publier sur TestPyPI puis PyPI

TestPyPI est l’instance de staging officielle pour tester un upload avant publication réelle. On publie d’abord là, on installe depuis là pour vérifier, puis on publie sur le vrai PyPI. Depuis 2022, l’authentification se fait par API tokens (jamais user/password) ou via trusted publishers (OIDC depuis GitHub Actions, sans token).

# Créer un token sur https://test.pypi.org/manage/account/token/
# puis https://pypi.org/manage/account/token/

# Configurer ~/.pypirc
cat > ~/.pypirc <<EOF
[distutils]
index-servers = pypi testpypi

[pypi]
username = __token__
password = pypi-AgEIc... (votre token)

[testpypi]
repository = https://test.pypi.org/legacy/
username = __token__
password = pypi-AgEIc... (votre token TestPyPI)
EOF
chmod 600 ~/.pypirc

# Publication TestPyPI
uv tool run twine upload --repository testpypi dist/*

# Test depuis TestPyPI
uv venv .check
.check/bin/pip install --index-url https://test.pypi.org/simple/     --extra-index-url https://pypi.org/simple itsc-tools

# Si OK, publication PyPI réelle
uv tool run twine upload dist/*

Trois disciplines critiques. Le token PyPI est limité au projet (création depuis Manage account → API tokens), pas un token global — si compromis, on régénère sans toucher au reste. Le --extra-index-url permet à TestPyPI de résoudre les dépendances depuis le vrai PyPI (sinon, échec sur httpx absent de TestPyPI). Toujours tester l’installation depuis TestPyPI avant de polluer PyPI réel — une version cassée publiée ne peut pas être remplacée, seulement yankée.

Étape 8 — Publication automatique depuis GitHub Actions (Trusted Publishing)

Le Trusted Publishing (OIDC) est la voie recommandée par PyPI depuis 2023 : zéro token stocké, GitHub Actions s’authentifie via OIDC, PyPI vérifie la provenance. Le workflow se déclenche sur un tag Git, build, et publie.

# .github/workflows/publish.yml
name: Publish
on:
  push:
    tags: ["v*"]

jobs:
  build-and-publish:
    runs-on: ubuntu-latest
    environment: release  # créé dans Settings → Environments
    permissions:
      id-token: write  # nécessaire pour OIDC
    steps:
      - uses: actions/checkout@v5
      - uses: astral-sh/setup-uv@v6
      - run: uv build
      - uses: pypa/gh-action-pypi-publish@release/v1
        # pas de token : OIDC trusted publisher

Côté PyPI, dans Manage project → Publishing, on déclare le repo GitHub + workflow + environnement comme Trusted Publisher. Désormais, créer un tag v0.4.0, pousser, et PyPI reçoit le package signé par GitHub. Plus aucun token à gérer, à régénérer, à risquer d’exposer. Pour une organisation, c’est le standard de sécurité 2026.

Erreurs fréquentes

Symptôme Cause Solution
Build vide ou minimaliste Mauvais chemin dans hatch.build.targets.wheel.packages Pointer vers src/mon_package, pas src/
Tests installés dans la wheel Pas de séparation src/tests Adopter src-layout strictement
README brisé sur PyPI Format non déclaré Ajouter content-type dans [project.readme] ou utiliser .md détecté auto
Classifier invalide → upload refusé Typo dans classifier Vérifier sur pypi.org/classifiers la chaîne exacte
Conflit de version sur PyPI Re-upload même version Incrémenter la version ; PyPI n’autorise jamais un re-upload
Token PyPI rejeté Username différent de __token__ username = __token__ exactement (avec underscores)

Foire aux questions

setuptools, hatchling, flit ou poetry ?
Hatchling pour projets neufs : moderne, configuration claire, plugins. Setuptools si vous avez du legacy ou besoin de C extensions. Flit pour les micro-projets pure-Python. Poetry encore largement utilisé mais uv + hatchling le supplante en 2026.

Faut-il publier en sdist et wheel ?
Oui, les deux. Wheel pour installation rapide, sdist pour qui veut compiler depuis source ou pour les distributions Linux qui repackagent.

Comment versionner depuis un tag Git ?
hatch-vcs avec tool.hatch.version.source = "vcs". Les commits non taggés sortent en 0.3.2.dev3+gabcdef automatiquement.

Comment ajouter des fichiers non-Python (templates, JSON) ?
Ajouter [tool.hatch.build.targets.wheel.force-include] avec les patterns ou utiliser include-package-data = true si setuptools.

Comment publier un package privé en interne ?
Self-host devpi, gitea-packages, ou utiliser AWS CodeArtifact / Azure Artifacts. Le client uv ou pip supportent --index-url avec credentials.

Pour aller plus loin

Le packaging maîtrisé, le projet Python est désormais complet : tests pytest avancés, asyncio en production, validation Pydantic v2, qualité Ruff/uv, distribution pyproject.toml. Revenir au guide principal Python pour la vue d’ensemble.

Ressources et références

Service ITSkillsCenter

Site ou application web sur mesure

Conception Pro + Nom de domaine 1 an + Hébergement 1 an + Formation + Support 6 mois. Accès et code livrés. À partir de 350 000 FCFA.

Demander un devis
Publicité