Aller au contenu
Développement medium

Python : empaqueter et publier un paquet sur PyPI

15 min de lecture

logo python

Publier un paquet Python sur PyPI tient en quatre gestes : structurer le projet avec un pyproject.toml, construire une distribution avec python -m build, la vérifier avec twine check, puis l'envoyer avec twine upload. Ce guide couvre le layout src/, le fichier de configuration au format PEP 621, la production d'un sdist et d'un wheel, la vérification avant envoi, la publication sur TestPyPI puis sur PyPI, et le versionnage sémantique. Il s'adresse aux développeurs qui ont déjà écrit un module réutilisable et veulent le distribuer proprement plutôt que de le copier-coller de projet en projet.

À la fin, vous saurez transformer un dossier de code en paquet installable via pip install, testé d'abord sur un dépôt bac à sable avant la mise en ligne définitive. Les exemples ont été exécutés avec Python 3.12, build 1.5.0 et twine 6.2.0 sur un paquet réel nommé slugify-fr.

  • Structurer un projet avec le layout src/ recommandé.
  • Rédiger un pyproject.toml conforme à la PEP 621.
  • Construire un sdist et un wheel avec python -m build.
  • Vérifier les distributions avec twine check avant tout envoi.
  • Publier sur TestPyPI puis PyPI avec des jetons d'API.
  • Choisir un numéro de version selon le versionnage sémantique.

Ce guide suppose une base Python déjà en place. Vous avez besoin d'un Python 3.9 ou plus récent, du gestionnaire pip à jour, et d'un module que vous voulez distribuer. Le travail se fait dans un environnement virtuel isolé pour ne pas polluer votre système.

  • Python 3.9+ installé et accessible en ligne de commande.
  • Un module fonctionnel que vous voulez partager.
  • Un compte gratuit sur TestPyPI et un sur PyPI pour la publication réelle.

Si les notions de module et d'environnement virtuel sont encore floues, le guide sur les modules et paquets Python pose les bases utiles avant d'aller plus loin. Pour gérer les dépendances et le cycle de vie d'un projet, uv et Poetry automatisent une partie de ce que ce guide fait à la main.

Un paquet publiable n'est pas un simple fichier .py. C'est un dossier organisé que les outils de construction savent lire. La disposition la plus fiable est le layout src/ : le code source vit dans un sous-dossier src/, séparé des fichiers de configuration à la racine.

  • Répertoireslugify-fr/
    • pyproject.toml
    • README.md
    • Répertoiresrc/
      • Répertoireslugify_fr/
        • init .py
        • core.py

Ce layout évite un piège classique : sans lui, Python peut importer le code depuis le dossier de travail au lieu de la version réellement installée, ce qui masque des erreurs d'empaquetage. En isolant le code dans src/, vos tests s'exécutent forcément contre le paquet installé, exactement comme chez l'utilisateur final. Le nom du dossier de code (slugify_fr) utilise des underscores, alors que le nom de distribution (slugify-fr) accepte les tirets ; c'est une convention courante et parfaitement valide.

Le fichier __init__.py marque le dossier comme paquet importable et expose l'API publique. Le nôtre déclare la fonction principale et la version.

"""slugify-fr : transforme un titre en slug URL propre."""
from .core import slugify
__all__ = ["slugify"]
__version__ = "0.1.0"

Le module core.py contient la logique réelle. Ici, une fonction qui normalise un titre en identifiant d'URL, sans accents ni caractères spéciaux.

import re
import unicodedata
def slugify(texte: str) -> str:
"""Convertit un texte en slug minuscule sans accents."""
texte = unicodedata.normalize("NFKD", texte)
texte = texte.encode("ascii", "ignore").decode("ascii")
texte = re.sub(r"[^a-zA-Z0-9]+", "-", texte).strip("-")
return texte.lower()

Le fichier pyproject.toml est le cœur de la configuration. Depuis la PEP 621, il centralise les métadonnées du projet dans une table [project] standardisée, comprise par tous les outils modernes. Fini les anciens setup.py impératifs : on décrit le paquet de façon déclarative.

Deux tables sont indispensables. La table [build-system] indique quel moteur construit le paquet ; nous utilisons hatchling, léger et sans configuration superflue. La table [project] porte le nom, la version, la description et les dépendances.

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "slugify-fr"
version = "0.1.0"
description = "Transforme un titre francais en slug URL propre"
readme = "README.md"
requires-python = ">=3.9"
license = "MIT"
authors = [{ name = "Stephane Robert" }]
keywords = ["slug", "url", "text"]
dependencies = []
[project.urls]
Homepage = "https://github.com/stephrobert/slugify-fr"

Chaque champ a un rôle précis. Le name doit être unique sur PyPI : vérifiez sa disponibilité avant de vous lancer. Le champ readme pointe vers le fichier affiché sur la fiche du paquet ; c'est la vitrine de votre projet. La clé requires-python fixe les versions d'interpréteur supportées et empêche l'installation sur un Python trop ancien. Le tableau dependencies liste les paquets tiers nécessaires ; vide ici, il contiendrait par exemple ["requests>=2.31"] pour un projet qui appelle une API.

La version n'est pas un détail cosmétique : c'est un contrat avec vos utilisateurs. Le versionnage sémantique (SemVer) encode ce contrat en trois nombres, MAJEUR.MINEUR.CORRECTIF, dont chacun signale un type de changement précis.

SegmentQuand l'incrémenterExemple
MAJEURChangement incompatible avec l'ancienne API1.0.0 vers 2.0.0
MINEURNouvelle fonctionnalité rétrocompatible1.2.0 vers 1.3.0
CORRECTIFCorrection de bug sans changement d'API1.3.0 vers 1.3.1

Notre paquet démarre en 0.1.0. La série 0.x signale un projet jeune, où l'API peut encore bouger sans prévenir : c'est une pratique attendue et acceptée avant la première version stable. Le passage à 1.0.0 marque l'engagement de stabilité. Point crucial : PyPI refuse de réécrire une version déjà publiée. Chaque envoi exige un numéro neuf, ce qui vous force à une hygiène saine de publication.

Une fois le projet décrit, la construction produit les fichiers distribuables. L'outil de référence est build, qui appelle le moteur déclaré dans pyproject.toml en environnement isolé. On l'installe avec twine, l'outil d'envoi que nous utiliserons juste après.

Fenêtre de terminal
python -m pip install --upgrade pip
pip install build twine

Le build génère deux formats complémentaires. Le sdist (source distribution) est une archive .tar.gz du code source, reconstruite chez l'utilisateur à l'installation. Le wheel est un format précompilé en .whl, installé directement sans étape de build : c'est le format rapide, privilégié par pip. On produit les deux d'une seule commande.

Fenêtre de terminal
python -m build

La sortie réelle détaille chaque étape, de la préparation de l'environnement isolé à la création des deux archives.

* Creating isolated environment: venv+pip...
* Installing packages in isolated environment:
- hatchling
* Getting build dependencies for sdist...
* Building sdist...
* Building wheel from sdist
* Getting build dependencies for wheel...
* Building wheel...
Successfully built slugify_fr-0.1.0.tar.gz and slugify_fr-0.1.0-py3-none-any.whl

Les artefacts atterrissent dans un dossier dist/ créé pour l'occasion. Un rapide listage confirme la présence des deux formats attendus.

Fenêtre de terminal
ls -1 dist/
slugify_fr-0.1.0-py3-none-any.whl
slugify_fr-0.1.0.tar.gz

Le fragment py3-none-any dans le nom du wheel indique un paquet pur Python, compatible avec tout Python 3, quel que soit le système d'exploitation. Un paquet contenant du code C compilé afficherait ici une plateforme précise à la place de any.

Avant tout envoi, twine check inspecte les archives produites. Il valide surtout que la description longue issue du README se rendra correctement sur la fiche PyPI. Un README mal formé passe le build sans broncher mais s'affiche cassé en ligne : cette vérification l'attrape avant publication.

Fenêtre de terminal
twine check dist/*

Sur nos deux archives, le contrôle renvoie PASSED pour chacune.

Checking dist/slugify_fr-0.1.0-py3-none-any.whl: PASSED
Checking dist/slugify_fr-0.1.0.tar.gz: PASSED

Ce filet de sécurité coûte une seconde et évite une fiche publique bancale. Intégrez-le systématiquement juste après python -m build, avant même de penser à l'envoi.

La règle d'or : jamais un premier envoi directement sur PyPI. On répète d'abord sur TestPyPI, un dépôt bac à sable au fonctionnement identique mais sans conséquence sur l'index public. Une erreur de métadonnée s'y corrige sans brûler un numéro de version sur le vrai index.

L'authentification passe par un jeton d'API, jamais par votre mot de passe. Un jeton se génère depuis les paramètres du compte (section « API tokens »), se limite à un projet, et se révoque en un clic s'il fuite. Le nom d'utilisateur est toujours la valeur littérale __token__, et le mot de passe est le jeton lui-même.

Le fichier ~/.pypirc centralise les deux dépôts sans exposer le secret en ligne de commande. Chaque password reçoit un jeton distinct.

[distutils]
index-servers =
pypi
testpypi
[testpypi]
repository = https://test.pypi.org/legacy/
username = __token__
password = pypi-VOTRE-JETON-TESTPYPI
[pypi]
username = __token__
password = pypi-VOTRE-JETON-PYPI

L'envoi vers TestPyPI cible explicitement le dépôt de test avec l'option --repository.

Fenêtre de terminal
twine upload --repository testpypi dist/*
  1. Créer les comptes et les jetons : inscrivez-vous sur TestPyPI et PyPI, activez la double authentification, puis générez un jeton d'API sur chaque plateforme.

  2. Envoyer sur TestPyPI : lancez twine upload --repository testpypi dist/* et vérifiez la fiche affichée sur test.pypi.org.

  3. Tester l'installation depuis TestPyPI dans un environnement neuf (section suivante) pour confirmer que le paquet s'installe et s'importe.

  4. Publier sur PyPI une fois tout validé, avec twine upload dist/* sans option de dépôt : c'est l'index public par défaut.

L'envoi définitif sur PyPI reprend la même commande, sans préciser de dépôt puisque PyPI est la cible par défaut.

Fenêtre de terminal
twine upload dist/*

Publier ne suffit pas : il faut prouver que le paquet s'installe et fonctionne depuis le dépôt, pas seulement en local. On crée un environnement virtuel neuf, vierge de tout code du projet, et on installe depuis TestPyPI.

Fenêtre de terminal
python -m venv /tmp/essai-slugify
source /tmp/essai-slugify/bin/activate
pip install --index-url https://test.pypi.org/simple/ slugify-fr

L'option --index-url pointe pip vers TestPyPI au lieu de l'index public. Si votre paquet a des dépendances absentes de TestPyPI, ajoutez --extra-index-url https://pypi.org/simple/ pour que pip les récupère sur l'index normal. Une fois installé, un import direct confirme que tout fonctionne.

Fenêtre de terminal
python -c "from slugify_fr import slugify; print(slugify('Ceci est un Titre a Slugifier !'))"
ceci-est-un-titre-a-slugifier

Ce test de bout en bout valide la chaîne complète : structure du projet, métadonnées, construction, envoi et installation. S'il passe sur TestPyPI, l'envoi sur PyPI se fera en confiance. C'est le dernier verrou avant la publication réelle.

Vérifiez que l'essentiel de ce guide est acquis. Les questions portent uniquement sur ce qui vient d'être expliqué ici.

Contrôle de connaissances

Validez vos connaissances avec ce quiz interactif

6 questions
6 min.
70% requis

Informations

  • Le chronomètre démarre au clic sur Démarrer
  • Questions à choix multiples, vrai/faux et réponses courtes
  • Vous pouvez naviguer entre les questions
  • Les résultats détaillés sont affichés à la fin

Lance le quiz et démarre le chronomètre

  • Le layout src/ isole le code et garantit que vos tests visent le paquet installé, pas le dossier de travail.
  • Le pyproject.toml au format PEP 621 décrit le paquet de façon déclarative : name, version, dependencies et build-system.
  • python -m build produit un sdist (.tar.gz) et un wheel (.whl) dans dist/.
  • twine check dist/* valide le rendu de la fiche avant tout envoi ; visez PASSED.
  • Publiez d'abord sur TestPyPI, testez l'installation, puis seulement sur PyPI.
  • Le versionnage sémantique (MAJEUR.MINEUR.CORRECTIF) signale la nature de chaque changement ; PyPI refuse de réécrire une version publiée.
  • Authentifiez-vous par jeton d'API avec l'utilisateur __token__, jamais un secret en clair dans le code ou Git.

Publier un paquet engage aussi la sécurité de la chaîne de distribution.

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracking. Un soutien, même symbolique, m'aide à couvrir l'hébergement et à garder ces ressources gratuites. Merci pour votre appui.

Le formulaire ne s'affiche pas ? Ouvrir Ko-fi dans un onglet.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn