backend / python
Packaging et publication sur PyPI
Explication
Ce que vous allez apprendre
- Transformer un dossier de code Python en package installable via
pip install - Décrire un projet complet dans
pyproject.toml(métadonnées, dépendances, scripts) - Comprendre pourquoi le "src layout" évite un piège classique lors des tests
- Construire et publier un package avec
python -m buildettwine - Appliquer le versionnement sémantique (MAJOR.MINOR.PATCH) pour communiquer l'impact d'une release
Dans quel contexte ?
Un développeur a écrit une bibliothèque interne de validation de données réutilisée dans trois projets de son entreprise, copiée-collée d'un dépôt à l'autre. À chaque correction de bug, il doit la recopier manuellement dans les trois projets, et deux d'entre eux finissent par diverger. Publier cette bibliothèque comme un vrai package installable (sur un registre interne ou PyPI) résout définitivement le problème : un pip install mon-outil-validation==1.2.0 identique partout, et une seule source de vérité à corriger.
D'un script à un package installable par tout le monde
Jusqu'à présent, votre code Python vit dans un dossier que vous exécutez directement. Mais dès qu'on veut le partager — avec une équipe, ou avec le monde entier via pip install — il faut le transformer en "package" : une unité installable, versionnée, avec des métadonnées claires (nom, dépendances, licence).
Le rôle de pyproject.toml
Ce fichier est aujourd'hui le point d'entrée standard de tout projet Python packagé. Il décrit deux choses distinctes : comment construire le package (build-system, quel outil transforme votre code source en artefact distribuable) et ce qu'est le package (project : nom, version, dépendances, auteurs). Cette séparation permet à différents outils de build (hatchling, setuptools, poetry...) de coexister avec un format de description commun.
Piège fréquent
Sans "src layout", lancer pytest depuis la racine du projet peut importer silencieusement votre code source non installé au lieu du package réellement installé par pip install -e .. Vos tests passent alors dans des conditions qui ne reflètent pas ce que vos utilisateurs installeront réellement.
Le "src layout" : un détail qui évite un vrai piège
Placer le code sous src/mon_package/ plutôt qu'à la racine peut sembler une contrainte arbitraire, mais elle évite un bug classique : sans ce dossier, quand vous lancez vos tests depuis la racine du projet, Python peut importer accidentellement votre code non installé (donc pas testé dans les conditions réelles d'installation) au lieu de la version réellement packagée.
Construire, vérifier, publier : un pipeline en étapes
python -m build produit deux formats : un wheel (.whl, prêt à l'emploi, rapide à installer) et une source distribution (.tar.gz, le code source brut). twine vérifie ces artefacts puis les envoie vers un registre. Publier d'abord sur TestPyPI (un registre "bac à sable") permet de repérer les erreurs avant la publication définitive : sur PyPI, un numéro de version supprimé ne peut jamais être réutilisé.
Versionnement sémantique
Le format MAJOR.MINOR.PATCH communique l'impact d'un changement à vos utilisateurs sans qu'ils aient à lire le changelog : un changement de MAJOR les prévient qu'ils doivent vérifier leur code avant de mettre à jour.
| Changement de version | Signifie | Exemple |
|---|---|---|
| PATCH (1.0.0 → 1.0.1) | Correction de bug, rétro-compatible | Un calcul erroné corrigé |
| MINOR (1.0.1 → 1.1.0) | Nouvelle fonctionnalité, rétro-compatible | Un nouveau paramètre optionnel |
| MAJOR (1.1.0 → 2.0.0) | Changement cassant | Suppression d'une fonction publique |
Bonne pratique
Publiez toujours d'abord sur TestPyPI (twine upload --repository testpypi dist/*) avant une publication définitive. Sur PyPI, un numéro de version supprimé ne peut jamais être réutilisé : une erreur détectée après coup vous oblige à publier immédiatement une version suivante plutôt que de corriger la précédente.
Commandes & code
Packaging et publication sur PyPI
Transformer un projet Python en package installable et le publier.
# --- Structure standard d'un package publiable (src layout, recommande) ---
# mon_package/
# pyproject.toml
# README.md
# LICENSE
# src/
# mon_package/
# __init__.py
# core.py
# py.typed # signale que le package fournit des type hints
# tests/
# test_core.py# pyproject.toml complet pour publication
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "mon-package-genial"
version = "0.1.0"
description = "Un package Python qui resout un vrai probleme"
readme = "README.md"
requires-python = ">=3.9"
license = "MIT"
authors = [
{name = "Alice Dupont", email = "alice@example.com"}
]
keywords = ["utilitaire", "cli", "exemple"]
classifiers = [
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
]
dependencies = [
"click>=8.0",
"requests>=2.28",
]
[project.urls]
Homepage = "https://github.com/alice/mon-package-genial"
Repository = "https://github.com/alice/mon-package-genial"
Issues = "https://github.com/alice/mon-package-genial/issues"
[project.scripts]
mon-cli = "mon_package.cli:main"
[project.optional-dependencies]
dev = ["pytest>=7.0", "build>=1.0", "twine>=4.0"]# src/mon_package/__init__.py
"""Mon package genial, expose son API publique."""
from mon_package.core import fonction_principale
__version__ = "0.1.0"
__all__ = ["fonction_principale"]# --- Cycle de build et publication ---
# 1. Installer les outils de build
pip install build twine
# 2. Construire les artefacts (wheel .whl et source distribution .tar.gz)
python -m build
# genere : dist/mon_package_genial-0.1.0-py3-none-any.whl
# dist/mon_package_genial-0.1.0.tar.gz
# 3. Verifier les artefacts avant publication
twine check dist/*
# 4. Publier d'abord sur TestPyPI pour valider (fortement recommande)
twine upload --repository testpypi dist/*
pip install --index-url https://test.pypi.org/simple/ mon-package-genial
# 5. Publier sur PyPI (production) -- IRREVERSIBLE pour ce numero de version
twine upload dist/*
# 6. Une fois publie, n'importe qui peut installer :
pip install mon-package-genial# --- Versionnement semantique (SemVer) : MAJOR.MINOR.PATCH ---
# 1.0.0 -> 1.0.1 : correction de bug (patch), retro-compatible
# 1.0.1 -> 1.1.0 : nouvelle fonctionnalite (minor), retro-compatible
# 1.1.0 -> 2.0.0 : changement cassant (major), rupture de compatibilite
# Automatiser le versionnement avec des tags git + CI
git tag v0.1.0
git push origin v0.1.0# .github/workflows/publish.yml : publication automatisee via GitHub Actions
name: Publish to PyPI
on:
release:
types: [published]
jobs:
build-and-publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install build twine
- run: python -m build
- run: twine upload dist/*
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}Résumé
- Le "src layout" (
src/mon_package/) évite les imports accidentels du code non installé pendant les tests. python -m buildgénère les artefacts standards (wheel + sdist),twineles publie.- Toujours valider sur TestPyPI avant une publication définitive sur PyPI (impossible de réutiliser un numéro de version supprimé).
- Suivre le versionnement sémantique (MAJOR.MINOR.PATCH) pour communiquer clairement l'impact de chaque release.
Exercices pratiques
Mission : industrialiser une bibliothèque copiée-collée dans trois projets
Objectif : Transformer un dossier de code dupliqué en package installable avec un src layout, et choisir le bon incrément de version sémantique pour une release cassante.
Contexte
Une bibliothèque interne de validation de données est copiée-collée dans trois projets de l'entreprise. À chaque correction de bug, un développeur doit la recopier manuellement dans les trois dépôts, et deux d'entre eux ont fini par diverger silencieusement, provoquant des bugs incohérents d'un projet à l'autre.
Tu dois organiser le code en package installable avec un src layout, choisir le bon type d'incrément de version pour un changement qui supprime une fonction publique, puis expliquer le piège classique évité par le src layout.