
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.
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- Structurer un projet avec le layout
src/recommandé. - Rédiger un
pyproject.tomlconforme à la PEP 621. - Construire un sdist et un wheel avec
python -m build. - Vérifier les distributions avec
twine checkavant tout envoi. - Publier sur TestPyPI puis PyPI avec des jetons d'API.
- Choisir un numéro de version selon le versionnage sémantique.
Prérequis
Section intitulée « Prérequis »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.
Structurer le projet avec le layout src/
Section intitulée « Structurer le projet avec le layout src/ »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 reimport 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()Décrire le paquet avec pyproject.toml
Section intitulée « Décrire le paquet avec pyproject.toml »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.
Choisir un numéro de version cohérent
Section intitulée « Choisir un numéro de version cohérent »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.
| Segment | Quand l'incrémenter | Exemple |
|---|---|---|
| MAJEUR | Changement incompatible avec l'ancienne API | 1.0.0 vers 2.0.0 |
| MINEUR | Nouvelle fonctionnalité rétrocompatible | 1.2.0 vers 1.3.0 |
| CORRECTIF | Correction de bug sans changement d'API | 1.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.
Construire le sdist et le wheel
Section intitulée « Construire le sdist et le wheel »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.
python -m pip install --upgrade pippip install build twineLe 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.
python -m buildLa 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.whlLes artefacts atterrissent dans un dossier dist/ créé pour l'occasion. Un rapide listage confirme la présence des deux formats attendus.
ls -1 dist/slugify_fr-0.1.0-py3-none-any.whlslugify_fr-0.1.0.tar.gzLe 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.
Vérifier les distributions avec twine check
Section intitulée « Vérifier les distributions avec twine check »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.
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: PASSEDChecking dist/slugify_fr-0.1.0.tar.gz: PASSEDCe 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.
Publier sur TestPyPI puis sur PyPI
Section intitulée « Publier sur TestPyPI puis sur PyPI »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-PYPIL'envoi vers TestPyPI cible explicitement le dépôt de test avec l'option --repository.
twine upload --repository testpypi dist/*-
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.
-
Envoyer sur TestPyPI : lancez
twine upload --repository testpypi dist/*et vérifiez la fiche affichée surtest.pypi.org. -
Tester l'installation depuis TestPyPI dans un environnement neuf (section suivante) pour confirmer que le paquet s'installe et s'importe.
-
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.
twine upload dist/*Installer depuis TestPyPI pour tester
Section intitulée « Installer depuis TestPyPI pour tester »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.
python -m venv /tmp/essai-slugifysource /tmp/essai-slugify/bin/activatepip install --index-url https://test.pypi.org/simple/ slugify-frL'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.
python -c "from slugify_fr import slugify; print(slugify('Ceci est un Titre a Slugifier !'))"ceci-est-un-titre-a-slugifierCe 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.
Contrôle de connaissances
Section intitulée « Contrôle de connaissances »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
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
Vérification
(0/0)Profil de compétences
Quoi faire maintenant
Ressources pour progresser
Des indices pour retenter votre chance ?
Nouveau quiz complet avec des questions aléatoires
Retravailler uniquement les questions ratées
Retour à la liste des certifications
À retenir
Section intitulée « À retenir »- Le layout
src/isole le code et garantit que vos tests visent le paquet installé, pas le dossier de travail. - Le
pyproject.tomlau format PEP 621 décrit le paquet de façon déclarative :name,version,dependenciesetbuild-system. python -m buildproduit un sdist (.tar.gz) et un wheel (.whl) dansdist/.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.
Pour aller plus loin
Section intitulée « Pour aller plus loin »Publier un paquet engage aussi la sécurité de la chaîne de distribution.
- Gérer les secrets en CI/CD : sécuriser les jetons de publication dans un pipeline automatisé.