Modules, paquets et uv#
Le système de modules#
Tout projet Python un tant soit peu sérieux finit par dépasser la taille d’un seul fichier. Le système de modules de Python répond à ce besoin en permettant de découper le code en unités logiques indépendantes, chacune encapsulant des définitions de fonctions, de classes, de constantes et d’initialisations.
Définition 29 (Module)
Un module Python est simplement un fichier .py. Lorsqu’il est importé, Python l’exécute de haut en bas, peuple un espace de noms avec les noms définis dans le fichier, et met cet espace de noms à disposition via l’objet module. Chaque module possède un attribut __name__ (valant "__main__" s’il est exécuté directement, ou le nom du fichier sinon) et __file__ (le chemin absolu du fichier).
L’instruction import charge le module et le place dans l’espace de noms courant sous son nom. La forme from ... import importe directement un ou plusieurs noms du module dans l’espace de noms courant, sans y placer le module lui-même.
# Import du module entier
import math
print(math.pi)
print(math.sqrt(2))
# Import de noms spécifiques
from math import pi, sqrt, factorial
print(pi)
print(sqrt(2))
print(factorial(10))
# Import avec alias
import collections as col
import itertools as it
print(col.Counter("abracadabra"))
print(list(it.islice(it.count(1), 5)))
3.141592653589793
1.4142135623730951
3.141592653589793
1.4142135623730951
3628800
Counter({'a': 5, 'b': 2, 'r': 2, 'c': 1, 'd': 1})
[1, 2, 3, 4, 5]
# from module import * importe tout ce qui est listé dans __all__
# (ou tout ce qui ne commence pas par _ si __all__ n'est pas défini)
# Cette forme est généralement déconseillée dans les scripts, car elle
# pollue l'espace de noms courant et rend les origines difficiles à retracer.
# Exploration du module importé
import math
print(f"Nom : {math.__name__}")
print(f"Fichier : {math.__file__}")
print(f"Version : {getattr(math, '__version__', 'N/A')}")
print(f"Quelques attributs : {[a for a in dir(math) if not a.startswith('_')][:10]}")
Nom : math
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
Cell In[3], line 9
7 import math
8 print(f"Nom : {math.__name__}")
----> 9 print(f"Fichier : {math.__file__}")
10 print(f"Version : {getattr(math, '__version__', 'N/A')}")
11 print(f"Quelques attributs : {[a for a in dir(math) if not a.startswith('_')][:10]}")
AttributeError: module 'math' has no attribute '__file__'
Mécanisme d’importation#
Lorsque Python rencontre import monmodule, il cherche le module dans plusieurs emplacements, dans cet ordre :
sys.modules— le cache des modules déjà importés (les imports répétés ne rechargent pas le fichier).Les modules intégrés (built-in modules) compilés dans l’interpréteur (
sys,builtins…).Les dossiers listés dans
sys.path.
sys.path est initialisé avec : le répertoire du script (ou "" pour le répertoire courant en mode interactif), la variable d’environnement PYTHONPATH, puis les dossiers de la bibliothèque standard et les dossiers des paquets installés.
import sys
# Les cinq premières entrées de sys.path
for chemin in sys.path[:5]:
print(chemin or "(répertoire courant)")
Paquets#
Un paquet est un dossier contenant un fichier __init__.py et, optionnellement, d’autres modules ou sous-paquets. Il permet d’organiser un ensemble de modules liés sous un espace de noms commun.
Définition 30 (Paquet Python)
Un paquet est un répertoire contenant un fichier __init__.py. Ce fichier est exécuté lorsque le paquet est importé ; il peut être vide ou contenir des initialisations et des réexportations. Les sous-paquets sont des répertoires imbriqués, eux aussi pourvus d’un __init__.py. Un paquet peut contenir des ressources non-Python (images, fichiers de configuration, données), accessibles via importlib.resources.
Voici un exemple de structure de paquet typique :
mon_paquet/
├── __init__.py # Initialisation du paquet
├── utils.py # Module utilitaires
├── modèles/
│ ├── __init__.py # Initialisation du sous-paquet
│ ├── utilisateur.py
│ └── produit.py
└── services/
├── __init__.py
└── paiement.py
Le fichier __init__.py joue un rôle important : il définit ce que l’on peut importer directement depuis le paquet, et peut réexporter des symboles de sous-modules pour offrir une API de surface propre.
# Simulation d'une structure de paquet en mémoire
# (dans un vrai projet, ce seraient des fichiers sur disque)
# Contenu typique d'un __init__.py :
exemple_init = '''
"""
mon_paquet — description du paquet.
"""
from .utils import fonction_utile # Réexporte depuis un sous-module
from .modèles.utilisateur import Utilisateur # Réexporte depuis un sous-paquet
__version__ = "1.0.0"
__all__ = ["fonction_utile", "Utilisateur"]
'''
print("Contenu d'un __init__.py typique :")
print(exemple_init)
__all__ — contrôle de l’API publique#
La variable de module __all__ est une liste de chaînes de caractères qui définit explicitement les noms exportés par from module import *. Elle constitue également une documentation implicite de l’API publique du module.
# Dans un module, on déclare __all__ pour contrôler ce qui est exporté
code_module = '''
__all__ = ["fonction_publique", "ClassePublique"] # Seuls ces noms sont exportés
def fonction_publique():
"""Fait partie de l'API publique."""
return "résultat public"
def _fonction_privée():
"""Convention : préfixe _ signale un détail d'implémentation."""
return "usage interne seulement"
class ClassePublique:
"""Partie de l'API publique."""
pass
class _ClasseInterne:
"""Détail d'implémentation."""
pass
'''
print(code_module)
Imports relatifs#
Dans un paquet, les modules peuvent se référencer mutuellement via des imports relatifs, utilisant des points (.) pour indiquer la position relative dans la hiérarchie du paquet.
# Dans mon_paquet/services/paiement.py :
exemple_imports_relatifs = '''
from . import utils # Importe utils depuis le même paquet
from .modèles import Utilisateur # Importe depuis un sous-paquet frère
from ..config import PARAMÈTRES # Importe depuis le paquet parent
# Les imports relatifs ne fonctionnent QUE dans un paquet,
# pas dans un script exécuté directement.
'''
print(exemple_imports_relatifs)
Paquets namespace (Python 3.3+)#
Depuis Python 3.3, Python supporte les paquets namespace : des répertoires sans __init__.py. Ils permettent à un paquet d’être réparti sur plusieurs répertoires ou même plusieurs dépôts, ce qui est utile dans certains contextes d’entreprise où différentes équipes contribuent à un espace de noms commun.
# Les paquets namespace sont détectés automatiquement.
# Si Python trouve un répertoire sans __init__.py, il crée un
# paquet namespace qui peut unifier plusieurs emplacements.
# Vérification du type de paquet (namespace vs régulier)
import importlib
import email # Module de la bibliothèque standard avec sous-paquets
print(f"email.__path__ : {email.__path__}")
print(f"email.__file__ : {getattr(email, '__file__', 'N/A (namespace)')}")
uv — le gestionnaire de projets moderne#
uv est un outil de gestion de projets et de paquets Python écrit en Rust par la société Astral. Il offre une interface unifiée pour créer des projets, gérer les dépendances, créer des environnements virtuels, exécuter du code, et publier des paquets sur PyPI. Sa vitesse — souvent dix à cent fois supérieure à celle de pip — et sa reproductibilité en font le gestionnaire de référence pour les projets Python modernes.
Initialiser un projet#
# Créer un nouveau projet avec la structure standard
uv init mon_projet
cd mon_projet
La commande uv init génère la structure suivante :
mon_projet/
├── .venv/ # Environnement virtuel (créé automatiquement)
├── .python-version # Version de Python fixée pour le projet
├── pyproject.toml # Métadonnées et dépendances du projet
├── uv.lock # Verrou de toutes les dépendances (généré par uv)
└── src/
└── mon_projet/
├── __init__.py
└── main.py
pyproject.toml#
Le fichier pyproject.toml est le standard moderne (PEP 517/518/621) de configuration des projets Python. Il remplace setup.py, setup.cfg, requirements.txt et une bonne part de tox.ini.
# Exemple de pyproject.toml pour un projet avec uv
exemple_toml = '''
[project]
name = "mon-projet"
version = "0.1.0"
description = "Description courte du projet"
readme = "README.md"
requires-python = ">=3.11"
license = { text = "MIT" }
authors = [
{ name = "Alice Dupont", email = "alice@exemple.fr" }
]
# Dépendances obligatoires
dependencies = [
"httpx>=0.27",
"pydantic>=2.0",
]
# Scripts installables (entry points)
[project.scripts]
mon-outil = "mon_projet.cli:main"
# Dépendances optionnelles (extras)
[project.optional-dependencies]
dev = ["pytest>=8", "ruff", "mypy"]
docs = ["sphinx", "furo"]
# Configuration de uv
[tool.uv]
dev-dependencies = [
"pytest>=8",
"ruff>=0.4",
]
# Configuration de ruff (linter)
[tool.ruff]
line-length = 100
target-version = "py311"
# Configuration de mypy
[tool.mypy]
strict = true
'''
print(exemple_toml)
Gérer les dépendances#
# Ajouter une dépendance (met à jour pyproject.toml et uv.lock)
uv add httpx
uv add "pandas>=2.0"
# Ajouter une dépendance de développement
uv add --dev pytest ruff mypy
# Ajouter une dépendance optionnelle (extra)
uv add --optional docs sphinx furo
# Supprimer une dépendance
uv remove httpx
# Synchroniser l'environnement avec les dépendances déclarées
uv sync
# Mettre à jour toutes les dépendances compatibles
uv lock --upgrade
Le fichier uv.lock fixe les versions exactes de toutes les dépendances directes et transitives. Il doit être versionné dans git pour garantir que tous les membres de l’équipe et tous les environnements CI/CD utilisent exactement les mêmes versions.
Exécuter du code#
# Exécuter un script dans l'environnement du projet
uv run mon_script.py
# Exécuter une commande (outils installés comme dépendances)
uv run pytest
uv run ruff check src/
uv run mypy src/
# Exécuter un outil sans l'installer dans le projet (uv tool)
uvx ruff check .
uvx black --check .
Environnements virtuels#
Un environnement virtuel est un répertoire isolé contenant une installation Python et ses paquets, indépendante du système. uv crée et gère automatiquement l’environnement .venv du projet, mais on peut aussi manipuler l’environnement directement.
# Créer explicitement un environnement virtuel
uv venv
# Créer un environnement avec une version spécifique de Python
uv venv --python 3.12
# Activer l'environnement (optionnel, uv run le gère automatiquement)
source .venv/bin/activate # Linux / macOS
.venv\Scripts\activate # Windows
# Lister les paquets installés
uv pip list
uv pip show numpy
Publier sur PyPI#
La publication d’un paquet sur PyPI suit un processus standardisé en quatre étapes :
# 1. Construire les distributions (wheel + sdist)
uv build
# 2. Vérifier la distribution (optionnel mais recommandé)
uvx twine check dist/*
# 3. Publier sur TestPyPI d'abord (bonne pratique)
uv publish --publish-url https://test.pypi.org/legacy/
# 4. Publier sur PyPI
uv publish
Remarque 30
Pour publier sur PyPI, vous devez créer un compte sur pypi.org et générer un token d’API. uv lit les identifiants depuis la variable d’environnement UV_PUBLISH_TOKEN ou depuis le fichier ~/.pypi/credentials. Il est fortement recommandé de ne jamais stocker de tokens dans le code source ou dans des fichiers versionnés.
```{prf:example} Structure complète d’un projet uv publiable
:label: example-16-01
Voici la structure recommandée pour un projet Python destiné à être publié sur PyPI :
mon-paquet/
├── .python-version # Fixe la version Python (ex: "3.12")
├── pyproject.toml # Métadonnées, dépendances, configuration des outils
├── uv.lock # Verrou des dépendances (versionné dans git)
├── README.md # Documentation principale (utilisée sur PyPI)
├── LICENSE # Fichier de licence (MIT, Apache-2.0…)
├── src/
│ └── mon_paquet/
│ ├── __init__.py # API publique, __version__
│ ├── _interne.py # Modules préfixés _ : usage interne
│ └── cli.py # Point d'entrée CLI (si applicable)
└── tests/
├── __init__.py
├── conftest.py # Fixtures pytest partagées
└── test_mon_paquet.py
La convention src/ (layout src) est recommandée pour les paquets publiés : elle garantit que les tests utilisent le paquet installé (via uv sync) plutôt que les fichiers locaux directement.
## Visualisation de l'écosystème `uv`
```{code-cell} python
:tags: [hide-input]
fig, ax = plt.subplots(figsize=(14, 9))
ax.set_xlim(0, 14)
ax.set_ylim(0, 9)
ax.axis('off')
ax.set_title("uv — flux de travail d'un projet Python moderne", fontsize=14,
fontweight='bold', pad=15)
palette = sns.color_palette("Set2", 7)
# Étapes principales
étapes = [
(0.4, 6.5, 2.6, 1.8, palette[0], "uv init", "Crée le projet,\npyproject.toml,\n.venv"),
(3.3, 6.5, 2.6, 1.8, palette[1], "uv add", "Ajoute des\ndépendances,\nmàj uv.lock"),
(6.2, 6.5, 2.6, 1.8, palette[2], "uv sync", "Synchronise\n.venv avec\nuv.lock"),
(9.1, 6.5, 2.6, 1.8, palette[3], "uv run", "Exécute dans\nl'environnement\ndu projet"),
(0.4, 3.5, 2.6, 1.8, palette[4], "uv build", "Construit\nwheel et\nsdist"),
(3.3, 3.5, 2.6, 1.8, palette[5], "uv publish", "Publie\nsur PyPI"),
(6.2, 3.5, 2.6, 1.8, palette[6], "uvx", "Exécute un\noutil sans\nl'installer"),
]
for (x, y, w, h, col, titre, desc) in étapes:
box = patches.FancyBboxPatch((x, y), w, h,
boxstyle="round,pad=0.15", linewidth=2,
edgecolor=col, facecolor=col, alpha=0.2)
ax.add_patch(box)
brd = patches.FancyBboxPatch((x, y), w, h,
boxstyle="round,pad=0.15", linewidth=2,
edgecolor=col, facecolor='none')
ax.add_patch(brd)
ax.text(x + w / 2, y + h - 0.35, titre,
ha='center', va='center',
fontsize=12, fontweight='bold', color=col,
fontfamily='monospace')
ax.text(x + w / 2, y + 0.55, desc,
ha='center', va='center',
fontsize=8.5, color='#333', style='italic')
# Flèches entre les étapes principales
for x_flèche in [3.0, 5.9, 8.8]:
ax.annotate('', xy=(x_flèche + 0.05, 7.4), xytext=(x_flèche - 0.05, 7.4),
arrowprops=dict(arrowstyle='->', color='#888', lw=2))
# Fichiers centraux
fichiers = [
(9.5, 3.0, 4.0, 1.5, "#888", "Fichiers clés"),
(9.5, 1.2, 4.0, 0.7, palette[0], "pyproject.toml"),
(9.5, 0.3, 4.0, 0.7, palette[2], "uv.lock"),
]
for (x, y, w, h, col, txt) in fichiers:
if txt == "Fichiers clés":
ax.text(x + w / 2, y + h / 2, txt, ha='center', va='center',
fontsize=10, fontweight='bold', color=col)
else:
box = patches.FancyBboxPatch((x, y), w, h,
boxstyle="round,pad=0.1", linewidth=1.5,
edgecolor=col, facecolor=col, alpha=0.15)
ax.add_patch(box)
ax.text(x + w / 2, y + h / 2, txt, ha='center', va='center',
fontsize=10, fontfamily='monospace', color=col)
plt.tight_layout()
plt.show()
Résumé#
Ce chapitre a exposé le système de modules de Python et les outils modernes de gestion de projets :
Un module est un fichier
.pydont l’importation exécute le code de haut en bas et peuple un espace de noms. Les instructionsimportetfrom ... importpermettent d’incorporer des modules et des noms spécifiques dans l’espace de noms courant. Python met en cache les modules danssys.modulespour éviter les rechargements répétés.Un paquet est un répertoire contenant
__init__.py. Ce fichier est exécuté à l’importation et peut réexporter des symboles pour construire une API de surface claire. La variable__all__contrôle explicitement ce qui est exporté parfrom paquet import *. Les imports relatifs (avec.) permettent à des modules d’un même paquet de se référencer sans dépendre du nom complet du paquet.Les paquets namespace (sans
__init__.py) permettent de répartir un paquet sur plusieurs emplacements.uvest le gestionnaire de projets moderne : il réunit en une seule commande la création de projets, la gestion des dépendances, la synchronisation des environnements virtuels, l’exécution de scripts et la publication sur PyPI.Le fichier
pyproject.toml(standard PEP 517/621) centralise les métadonnées du projet, ses dépendances, ses scripts et la configuration de tous les outils (ruff, mypy, pytest…).Le fichier
uv.lockgarantit la reproductibilité : il fixe les versions exactes de toutes les dépendances directes et transitives et doit être versionné dans git.La commande
uv publishprend en charge le processus de publication sur PyPI, après queuv builda construit les distributions.
Dans le chapitre suivant, nous synthétiserons les bonnes pratiques et idiomes pythoniques : le style PEP 8, les compréhensions, l’unpacking, les f-strings avancées, les outils de collections et contextlib, ainsi que les anti-patterns les plus courants à éviter.