Tutoriel Python - Partie 3 : Packaging et publication

← Partie 2 : Environnements virtuels | Partie 1 : Syntaxe Python | Retour aux tutoriels

Objectif: transformer un script en package Python réutilisable, versionnable et publiable proprement.

Parcours d'apprentissage (novice vers expert)

Fonctionnement: exécute les commandes dans l'ordre, contrôle les "Sortie attendue", puis applique le défi final.

1. Quand passer au packaging ?

Exemple concret

Ton dossier contient un utilitaire de validation d'emails. Tu veux l'importer dans 3 APIs différentes. Le packaging évite les copier-coller.

2. Structure moderne d'un package Python

email-tools/
├── pyproject.toml
├── README.md
├── LICENSE
├── src/
│   └── email_tools/
│       ├── __init__.py
│       └── validator.py
└── tests/
    └── test_validator.py

Pourquoi la structure src/ ?

3. pyproject.toml minimal et propre

[build-system]
requires = ["setuptools>=70", "wheel"]
build-backend = "setuptools.build_meta"

[project]
name = "email-tools-cf"
version = "0.1.0"
description = "Outils de validation d'emails"
readme = "README.md"
requires-python = ">=3.10"
authors = [
  {name = "Coder Facile", email = "contact@example.com"}
]
dependencies = [
  "email-validator>=2.2.0,<3.0"
]

[project.optional-dependencies]
dev = [
  "pytest>=8.0,<9.0",
  "ruff>=0.8,<1.0"
]

[tool.setuptools.packages.find]
where = ["src"]

Code exemple

# src/email_tools/validator.py
from email_validator import validate_email, EmailNotValidError


def is_valid(email: str) -> bool:
    try:
        validate_email(email, check_deliverability=False)
        return True
    except EmailNotValidError:
        return False

4. Build local: wheel + source distribution

# Dans l'environnement virtuel actif
py -m pip install --upgrade pip build
py -m build

Sortie attendue: un dossier dist/ est créé avec un fichier .whl et un .tar.gz.

Erreur fréquente: erreur de métadonnées pyproject.toml. Solution: vérifier name, version, et la section build-system.

Résultat attendu dans dist/:

dist/
├── email_tools_cf-0.1.0-py3-none-any.whl
└── email_tools_cf-0.1.0.tar.gz

Tester le package construit

# Nouveau venv de test
py -m venv .venv-test
.\.venv-test\Scripts\Activate.ps1
py -m pip install dist\email_tools_cf-0.1.0-py3-none-any.whl

# Test rapide
py -c "from email_tools.validator import is_valid; print(is_valid('a@b.com'))"

Sortie attendue: l'import fonctionne et la commande affiche True.

Erreur fréquente: ModuleNotFoundError après installation. Solution: vérifier que le package contient bien le dossier src/email_tools avec __init__.py.

5. Publication: TestPyPI puis PyPI

5.1 Installer Twine

py -m pip install twine

Sortie attendue: twine est installé dans l'environnement actif.

5.2 Vérifier les métadonnées avant upload

py -m twine check dist/*

Sortie attendue: message "PASSED" pour tous les artefacts.

Erreur fréquente: README mal formaté. Solution: corriger README.md puis régénérer le build.

5.3 Upload vers TestPyPI (recommandé en premier)

py -m twine upload --repository testpypi dist/*

Sortie attendue: upload réussi et URL de package sur TestPyPI.

Erreur fréquente: 403 Forbidden. Solution: vérifier le token TestPyPI et les droits associés.

5.4 Upload vers PyPI

py -m twine upload dist/*

Sortie attendue: package visible sur PyPI avec la version publiée.

Erreur fréquente: "File already exists". Solution: incrémenter la version dans pyproject.toml puis rebuild.

Bonnes pratiques de sécurité

6. Publication interne en entreprise

En entreprise, on publie souvent sur un registre privé (Nexus, Artifactory, Azure Artifacts, GitLab Package Registry).

Exemple de configuration .pypirc

[distutils]
index-servers =
    interne

[interne]
repository = https://packages.example.com/repository/pypi-internal/
username = __token__
password = pypi-XXXXXXXXXXXXXXXXXXXXXXXX

Upload vers registre interne

py -m twine upload --repository interne dist/*

Sortie attendue: artefacts disponibles dans le registry interne de l'entreprise.

Erreur fréquente: erreur TLS/certificat. Solution: vérifier certificats de l'infra et l'URL exacte du registre.

7. Versioning et cycle de release

Règle simple de version

Workflow release concret

# 1) Monter version dans pyproject.toml
# version = "0.2.0"

# 2) Commit + tag git
git add .
git commit -m "release: v0.2.0"
git tag v0.2.0

# 3) Build + checks + upload
py -m build
py -m twine check dist/*
py -m twine upload dist/*

Sortie attendue: tag git créé, artefacts rebuildés, publication acceptée pour la nouvelle version.

Erreur fréquente: tag déjà existant. Solution: choisir une nouvelle version sémantique et recréer le cycle de release.

8. Mini labo packaging: script vers package

Tu vas transformer un script local en package installable.

8.1 Départ

mkdir labo-package
cd labo-package
py -m venv .venv
.\.venv\Scripts\Activate.ps1
py -m pip install --upgrade pip build twine

Sortie attendue: environnement de travail prêt avec build et twine disponibles.

8.2 Créer les fichiers

# pyproject.toml
[build-system]
requires = ["setuptools>=70", "wheel"]
build-backend = "setuptools.build_meta"

[project]
name = "hello-cf-tool"
version = "0.1.0"
description = "Mini outil exemple"
requires-python = ">=3.10"

[tool.setuptools.packages.find]
where = ["src"]
# src/hello_cf_tool/main.py
def hello(name: str) -> str:
    return f"Bonjour {name}, package OK"

# src/hello_cf_tool/__init__.py
from .main import hello

8.3 Build et test local

py -m build
py -m pip install dist\hello_cf_tool-0.1.0-py3-none-any.whl
py -c "from hello_cf_tool import hello; print(hello('Equipe'))"

Sortie attendue: la commande Python affiche "Bonjour Equipe, package OK".

Erreur fréquente: nom du wheel introuvable. Solution: vérifier le nom réel dans dist/ et adapter la commande d'installation.

8.4 Critère de réussite

9. Résumé

Tu sais maintenant structurer un package Python, le builder, le tester et le publier proprement.

Défi progression (vers expert)

Mission: industrialiser ton package avec une vraie chaîne de release.

Partie 1 : Syntaxe Python | ← Revenir à la Partie 2 | Retour aux tutoriels