Retour au cours

backend / python

Packaging et publication sur PyPI

Leçon 291 exercice

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 build et twine
  • 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 versionSignifieExemple
PATCH (1.0.0 → 1.0.1)Correction de bug, rétro-compatibleUn calcul erroné corrigé
MINOR (1.0.1 → 1.1.0)Nouvelle fonctionnalité, rétro-compatibleUn nouveau paramètre optionnel
MAJOR (1.1.0 → 2.0.0)Changement cassantSuppression 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.

bash
# --- 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
toml
# 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"]
python
# 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"]
bash
# --- 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
bash
# --- 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
yaml
# .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 build génère les artefacts standards (wheel + sdist), twine les 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

1 disponible
1

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.

Résoudre l’exercice →