Python : Packaging
Publier son code Python sur PyPI c'est le rendre accessible à des milliers de développeurs. Exploration des concepts fondamentaux du packaging Python.
Qu'est-ce qu'un Package Python ?
Différence : Module vs Package
Module : Un fichier Python unique
calculator.py # C'est un module
Package : Un dossier contenant des modules
calculator/
├── __init__.py # Marque le dossier comme package
├── operations.py
├── utils.py
└── constants.py
Le fichier __init__.py est crucial : c'est ce qui dit à Python "je suis un package".
Structure simple
my_package/
├── my_package/ # Code source
│ ├── __init__.py
│ ├── core.py
│ └── utils.py
├── tests/ # Tests
├── README.md
├── LICENSE
└── pyproject.toml # Configuration de packaging
Dépendances du package
Un package peut dépendre d'autres packages (importés via pip install).
# Dans my_package/core.py
import requests # Dépendance externe
from .utils import helper # Dépendance interne
Ces dépendances externes doivent être déclarées lors du packaging.
Distributions : Wheel vs Source
Quand on publie un package, on crée deux types de distribution :
Source Distribution (sdist)
Format : my_package-1.0.0.tar.gz (ou .zip)
my_package-1.0.0/
├── my_package/
│ ├── __init__.py
│ ├── core.py
│ └── utils.py
├── setup.py
├── README.md
└── pyproject.toml
Avantages ✅
- Contient le code source complet
- Portable sur tous les OS/architectures
- Permet inspection du code
Inconvénients ❌
- Installation lente (compilation nécessaire)
- Requiert les build tools (compilateur C, etc.)
- Plus volumineux
Wheel Distribution (bdist_wheel)
Format : my_package-1.0.0-py3-none-any.whl (archive ZIP)
my_package-1.0.0.dist-info/
├── METADATA
├── RECORD
├── entry_points.txt
└── top_level.txt
my_package/
├── __init__.py
├── core.py
└── utils.py
Avantages ✅
- Installation ultra-rapide (pas de compilation)
- Ne requiert que pip
- Cohérent sur tous les environnements
Inconvénients ❌
- Spécifique à une version Python/plateforme
- Code pré-compilé (moins d'inspection)
Verdict : Toujours publier les deux. Wheel en priorité, source en fallback.
Métadonnées : Déclarer un Package
Les métadonnées c'est tout ce qu'on doit savoir sur un package : nom, version, dépendances, auteur, licence, etc.
Configuration avec pyproject.toml (Modern)
Depuis PEP 517/518 (2015+), c'est l'approche moderne :
[build-system]
requires = ["setuptools>=61.0", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "my-awesome-lib"
version = "1.0.0"
description = "Une libraire incroyable"
readme = "README.md"
requires-python = ">=3.8"
license = {text = "MIT"}
authors = [
{name = "John Doe", email = "john@example.com"}
]
keywords = ["awesome", "library", "python"]
# URLs
[project.urls]
Homepage = "https://github.com/user/my-awesome-lib"
Documentation = "https://my-awesome-lib.readthedocs.io"
Repository = "https://github.com/user/my-awesome-lib"
# Dépendances
[project.dependencies]
requests = ">=2.28.0"
pydantic = ">=1.10"
# Dépendances optionnelles
[project.optional-dependencies]
database = ["sqlalchemy>=1.4", "psycopg2>=2.9"]
email = ["aiosmtplib>=2.0"]
# Scripts CLI
[project.scripts]
my-cli = "my_lib.cli:main"
# Classifiers
[project.classifiers]
"Development Status :: 4 - Beta"
"Intended Audience :: Developers"
"License :: OSI Approved :: MIT License"
"Programming Language :: Python :: 3"
"Programming Language :: Python :: 3.8"
"Programming Language :: Python :: 3.9"
"Programming Language :: Python :: 3.10"
Configuration avec setup.py (Legacy)
Encore utilisé, particulièrement pour les extensions C :
from setuptools import setup, find_packages
setup(
name="my-awesome-lib",
version="1.0.0",
description="Une libraire incroyable",
author="John Doe",
author_email="john@example.com",
url="https://github.com/user/my-awesome-lib",
long_description=open("README.md").read(),
long_description_content_type="text/markdown",
license="MIT",
packages=find_packages(),
python_requires=">=3.8",
install_requires=[
"requests>=2.28.0",
"pydantic>=1.10",
],
extras_require={
"database": ["sqlalchemy>=1.4", "psycopg2>=2.9"],
"email": ["aiosmtplib>=2.0"],
},
entry_points={
"console_scripts": [
"my-cli=my_lib.cli:main",
],
},
classifiers=[
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3.8",
],
)
Configuration avec setup.cfg (Alternative)
Format INI, utile pour projects complexes :
[metadata]
name = my-awesome-lib
version = 1.0.0
description = Une libraire incroyable
author = John Doe
author_email = john@example.com
url = https://github.com/user/my-awesome-lib
long_description = file: README.md
long_description_content_type = text/markdown
license = MIT
[options]
packages = find:
python_requires = >=3.8
install_requires =
requests>=2.28.0
pydantic>=1.10
[options.extras_require]
database =
sqlalchemy>=1.4
psycopg2>=2.9
email =
aiosmtplib>=2.0
[options.entry_points]
console_scripts =
my-cli = my_lib.cli:main
Building : Créer les Distributions
Installer les outils
pip install setuptools wheel build
build est l'outil moderne et recommandé pour créer distributions.
Créer wheel + sdist
python -m build
Génère dans le dossier dist/ :
my_package-1.0.0-py3-none-any.whlmy_package-1.0.0.tar.gz
Vérifier la distribution
# Lister le contenu du wheel
unzip -l dist/my_package-1.0.0-py3-none-any.whl
# Lister le contenu du sdist
tar -tzf dist/my_package-1.0.0.tar.gz
PyPI : La Registry Centrale
Qu'est-ce que PyPI ?
Python Package Index : Registry centrale où vivent tous les packages Python publics.
- URL : https://pypi.org
- Packages : Environ 500k packages
- Téléchargements/jour : Millions
C'est là qu'on publie avec pip install le-package.
Créer un compte
- Aller sur https://pypi.org/account/register/
- Vérifier l'email
- Activer 2FA (recommandé)
- Générer un token API : https://pypi.org/account/tokens/
TestPyPI : Sandbox
Pour tester avant vraie publication.
- URL : https://test.pypi.org
- Compté séparé : Faut aussi s'y enregistrer
- Token séparé : À générer sur https://test.pypi.org/account/tokens/
Utile pour tester le processus de publication sans polluer PyPI.
Publication sur PyPI
Installation du CLI
pip install twine
twine est l'outil de publication, plus robust que python setup.py upload (dépréciée).
Configuration des Credentials
Option 1 : Token API (recommandé)
# Dans ~/.pypirc
[distutils]
index-servers =
pypi
[pypi]
repository = https://upload.pypi.org/legacy/
username = __token__
password = pypi-AgEIcHlwaS5vcmc... # Votre token
Option 2 : Username/password (moins sûr, legacy)
[pypi]
username = john
password = mon_mot_de_passe_clair # Mauvaise idée !
Publier sur TestPyPI
D'abord, ajouter TestPyPI à ~/.pypirc :
[distutils]
index-servers =
pypi
test-pypi
[test-pypi]
repository = https://test.pypi.org/legacy/
username = __token__
password = pypi-AgEIcHlwaS5vcm9qZWN0...
Puis publier :
python -m twine upload --repository test-pypi dist/*
Tester l'installation
# Depuis TestPyPI
pip install --index-url https://test.pypi.org/simple/ my-awesome-lib
# Depuis PyPI
pip install my-awesome-lib
Publier sur PyPI Official
python -m twine upload dist/*
Ou avec version spécifique :
python -m twine upload dist/my_package-1.0.0*
Semantic Versioning
Format : MAJOR.MINOR.PATCH[-pre-release][+build]
1.0.0 # Release stable
1.0.1 # Bugfix (PATCH)
1.1.0 # Feature (MINOR)
2.0.0 # Breaking change (MAJOR)
1.0.0-beta.1 # Pre-release (beta/alpha/rc)
1.0.0+build.1 # Build metadata
Règles :
MAJOR: Breaking change, code client doit changerMINOR: Feature rétro-compatiblePATCH: Bugfix rétro-compatible
Metadata complètes
init.py
# my_lib/__init__.py
__version__ = "1.0.0"
__author__ = "John Doe"
__email__ = "john@example.com"
__license__ = "MIT"
# Exposer l'API publique
from .core import main_function
from .utils import helper
__all__ = ["main_function", "helper"]
Classifiers importants
[project.classifiers]
# Status
"Development Status :: 3 - Alpha"
"Development Status :: 4 - Beta"
"Development Status :: 5 - Production/Stable"
# Licence
"License :: OSI Approved :: MIT License"
"License :: OSI Approved :: Apache Software License"
# Public
"Intended Audience :: Developers"
"Intended Audience :: System Administrators"
# Topics
"Topic :: Software Development"
"Topic :: System :: Monitoring"
# Python versions
"Programming Language :: Python :: 3"
"Programming Language :: Python :: 3.8"
"Programming Language :: Python :: 3.9"
"Programming Language :: Python :: 3.10"
"Programming Language :: Python :: 3.11"
Checklist Avant Publication
- ✅ Tests passent :
pytest - ✅ Code formaté et linté
- ✅ Version mise à jour (semantic versioning)
- ✅ CHANGELOG.md complété
- ✅ README.md avec instructions d'installation/usage
- ✅ LICENSE.md présent
- ✅ Métadonnées complètes dans pyproject.toml/setup.py
- ✅ Testé sur TestPyPI d'abord
- ✅ Tag Git :
git tag v1.0.0 - ✅ Commit des changements
- ✅ Build généré :
python -m build
Workflow Complet avec setuptools
# 1. Initialiser la structure
mkdir my-awesome-lib && cd my-awesome-lib
git init
# 2. Créer pyproject.toml et source
cat > pyproject.toml << 'EOF'
[build-system]
requires = ["setuptools>=61.0", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "my-awesome-lib"
version = "1.0.0"
description = "Une libraire incroyable"
requires-python = ">=3.8"
dependencies = ["requests>=2.28.0"]
EOF
mkdir my_lib
touch my_lib/__init__.py
# 3. Tester le build
pip install build
python -m build
# 4. Vérifier
pip install --dry-run dist/my_awesome_lib-1.0.0-py3-none-any.whl
# 5. Publier sur TestPyPI
twine upload --repository test-pypi dist/*
# 6. Tester installation
pip install --index-url https://test.pypi.org/simple/ my-awesome-lib
# 7. Publier sur PyPI official
twine upload dist/*
Bonnes Pratiques
Dépendances
# BON : Version mineure fixée
requests = ">=2.28.0,<3.0"
pydantic = ">=1.10,<2.0"
# MAUVAIS : Pas de limite sup
requests = ">=2.28.0"
# BON : Version exacte pour stabilité
some-critical-lib = "1.2.3"
Namespace packages
Utile si on maintient plusieurs packages liés :
src/
├── mycompany/
│ ├── __init__.py (empty!)
│ ├── lib1/
│ │ └── __init__.py
│ └── lib2/
│ └── __init__.py
[tool.setuptools.packages]
find = {where = ["src"]}
Alors from mycompany.lib1 import ... ça marche.
Entry points / Scripts CLI
[project.scripts]
my-cli = "my_lib.cli:main"
magic-tool = "my_lib.tools:run_magic"
L'installation du package rend ces commandes disponibles partout :
pip install my-awesome-lib
my-cli --help # Fonctionne!
magic-tool config # Fonctionne!
Extras / Optional dependencies
[project.optional-dependencies]
database = ["sqlalchemy>=1.4"]
email = ["aiosmtplib>=2.0"]
dev = ["pytest", "black", "mypy"]
Installation sélective :
pip install my-awesome-lib # Bare minimum
pip install my-awesome-lib[database] # + database
pip install my-awesome-lib[database,email] # + database et email
pip install my-awesome-lib[dev] # + all dev tools
Alternatives Modernes (Optionnel)
Poetry
Si vous préférez une approche all-in-one avec lock file :
poetry new my-lib
poetry add requests pydantic
poetry build && poetry publish
Poetry gère pyproject.toml, dependencies, et publication automatiquement. Idéal si vous aimez la cohésion.
uv
Package manager ultra-rapide (écrit en Rust) :
uv pip install requests
uv venv
Remplace pip pour des workflows rapides. Toujours utilise pyproject.toml/setup.py pour packages, juste accélère les installations.
Les deux restent compatibles avec le système d'emballage standard (wheel, sdist, PyPI, setuptools). Juste des wrapper/helpers autour.
Résources
- Official Packaging Guide
- setuptools Documentation
- PEP 427 - Wheel Format
- PEP 440 - Versioning
- PyPI Classifiers
- Twine Documentation
Conclusion
Le packaging Python s'appuie sur des concepts simples : modules, packages, distributions (wheel/sdist), métadonnées, et PyPI. Une fois qu'on comprend ça, publier son code devient facile. setuptools + twine suffisent pour la plupart des cas. Les alternatives modernes comme Poetry offrent plus de confort mais reposent toujours sur les mêmes fondations.
À vos pipelines! 🚀