Virtual environment, pip et pyproject.toml en Python
1. Vue d’ensemble
Lorsque vous travaillez sur plusieurs projets Python sur la même machine, vous rencontrerez rapidement des conflits de versions. Le projet A nécessite requests 2.28, tandis que le projet B a besoin de requests 2.32. Cet article couvre trois outils pour résoudre ce problème. venv isole l’environnement pour chaque projet. pip installe et gère les packages. pyproject.toml déclare le projet selon les normes modernes. Après avoir lu cet article, vous pourrez construire un projet Python avec une structure standard, installable en mode editable et avec des tests exécutables via pytest.
2. Virtual environment
Un virtual environment (venv) est un répertoire contenant trois choses : une copie de l’interpréteur Python (généralement un symlink), un répertoire site-packages propre et des scripts d’activation. Les packages que vous installez dans le venv résident uniquement dans ce répertoire, sans affecter le Python du système.
2.1. Créer et activer un venv
Le module venv est inclus dans la stdlib depuis Python 3.3, donc vous n’avez rien à installer de plus :
mkdir shopcart && cd shopcart
python3 -m venv .venv
# Linux / macOS
source .venv/bin/activate
# Windows (PowerShell)
.venv\Scripts\Activate.ps1
La commande python3 -m venv .venv crée un répertoire .venv dans votre projet. Le nom .venv est une convention commune, et la plupart des IDE le reconnaîtront automatiquement. Après l’activation, le prompt de votre shell aura le préfixe (.venv). Les commandes python et pip pointent maintenant vers le venv.
Pour quitter le venv, exécutez :
deactivate
2.2. Vérifier quel interpréteur vous utilisez
C’est une étape que beaucoup de gens oublient, et c’est la source de nombreuses erreurs du type « j’ai installé mais toujours ModuleNotFoundError » :
which python
python -c "import sys; print(sys.prefix); print(sys.prefix != sys.base_prefix)"
/home/lan/shopcart/.venv/bin/python
/home/lan/shopcart/.venv
True
sys.prefix est le répertoire racine de l’environnement actif. sys.base_prefix est le répertoire du Python système. Si ces deux valeurs diffèrent, vous êtes dans un venv.
Remarque : Ne commitez pas le répertoire
.venvsur Git. Ajoutez.venv/à votre.gitignore. Les venv dépendent des chemins absolus et du système d’exploitation, donc les copier sur une autre machine cassera généralement les choses. La bonne pratique est de committer le fichier de déclaration des dépendances et de recréer le venv sur le nouveau système.
3. Gérer les packages avec pip
pip est le gestionnaire de packages par défaut qui récupère les packages sur PyPI.
3.1. Commandes de base
python -m pip install --upgrade pip
python -m pip install "requests>=2.31,<3"
python -m pip list
python -m pip show requests
python -m pip uninstall -y requests
Voici ce que fait chaque commande :
install --upgrade pipmet à jour pip lui-même. Les anciennes versions de pip ne lisent pas toujours correctementpyproject.toml."requests>=2.31,<3"est un version specifier. Vous devez le mettre entre guillemets pour que le shell n’interprète pas<comme une redirection.listénumère les packages installés.showaffiche les détails, y compris la version, le chemin d’installation et les dépendances.
3.2. Pourquoi utiliser python -m pip au lieu de pip
La commande pip seule peut pointer vers une version Python différente de celle que vous utilisez. Cela arrive souvent si vous avez plusieurs versions de Python, ou si vous avez oublié d’activer votre venv. python -m pip exécute toujours le pip du bon interpréteur, de sorte que les packages sont installés dans l’environnement attendu.
3.3. requirements.txt et pip freeze
La façon traditionnelle de partager une liste de packages est d’utiliser requirements.txt :
python -m pip freeze > requirements.txt
python -m pip install -r requirements.txt
certifi==2024.8.30
charset-normalizer==3.4.0
idna==3.10
requests==2.32.3
urllib3==2.2.3
pip freeze enregistre tous les packages du venv avec des versions exactes, y compris les dépendances indirectes comme urllib3. Cette approche est utile pour reproduire exactement un environnement. Cependant, elle ne dit pas quels packages vous avez vraiment besoin ou quels packages viennent de dépendances transitives. Elle ne décrit pas non plus votre projet : son nom, sa version, les exigences Python. C’est le rôle de pyproject.toml.
4. Déclarer un projet avec pyproject.toml
pyproject.toml est le fichier de configuration standard pour un projet Python. Il est défini par les PEP 518, PEP 621 et les PEP connexes. Ce fichier remplace setup.py, setup.cfg et consolide la configuration pour de nombreux outils comme pytest ou ruff en un seul endroit.
4.1. Structure du projet
Nous utilisons la structure src layout, c’est-à-dire que le code est placé dans src/. Cette disposition garantit que les tests s’exécutent sur le package installé, et non en important accidentellement le répertoire de travail :
shopcart/
├── pyproject.toml
├── src/
│ └── shopcart/
│ ├── __init__.py
│ ├── order.py
│ └── cli.py
└── tests/
└── test_order.py
4.2. Fichier pyproject.toml complet
[build-system]
requires = ["setuptools>=69"]
build-backend = "setuptools.build_meta"
[project]
name = "shopcart"
version = "0.1.0"
description = "Calculer simplement le total des commandes"
requires-python = ">=3.11"
dependencies = [
"requests>=2.31,<3",
]
[project.optional-dependencies]
dev = [
"pytest>=8",
"ruff>=0.5",
]
[project.scripts]
shopcart = "shopcart.cli:main"
[tool.pytest.ini_options]
testpaths = ["tests"]
Voici ce que chaque section signifie :
[build-system]dit à pip quels outils sont nécessaires pour construire le projet. Ici, c’est setuptools. Vous pourriez la remplacer parhatchlingouflit_coresans modifier le reste.[project]contient les métadonnées standard.requires-pythonempêche l’installation sur des versions Python non supportées.dependenciesliste les packages obligatoires pour exécuter le projet. Vous devez déclarer des plages de versions (>=2.31,<3) plutôt que des versions fixes (==2.32.3).[project.optional-dependencies]regroupe les packages optionnels. Le groupedevcontient les outils de développement que l’utilisateur final n’a pas besoin d’avoir.[project.scripts]crée une commandeshopcartdans le terminal. Cette commande appelle la fonctionmaindu moduleshopcart.cli.[tool.pytest.ini_options]configure pytest. Chaque outil a son propre namespace[tool.<name>].
Avec le src layout, setuptools trouve automatiquement les packages dans src/, donc vous n’avez rien d’autre à déclarer.
4.3. Code du package
# src/shopcart/order.py
from dataclasses import dataclass, field
from decimal import Decimal
@dataclass(frozen=True)
class OrderLine:
product: str
unit_price: Decimal
quantity: int
@dataclass
class Order:
customer: str
lines: list[OrderLine] = field(default_factory=list)
def add(self, product: str, unit_price: str, quantity: int = 1) -> None:
if quantity <= 0:
raise ValueError("la quantité doit être supérieure à 0")
self.lines.append(OrderLine(product, Decimal(unit_price), quantity))
def total(self) -> Decimal:
return sum((l.unit_price * l.quantity for l in self.lines), Decimal("0"))
Order contient une liste de OrderLine. Les prix utilisent Decimal pour éviter les erreurs d’arrondi en virgule flottante, donc unit_price est accepté sous forme de chaîne. La méthode total() additionne les montants de chaque ligne, en commençant par Decimal("0").
# src/shopcart/cli.py
from shopcart.order import Order
def main() -> None:
order = Order(customer="Nguyễn Văn An")
order.add("Café au lait", "29000", 2)
order.add("Bánh mì", "25000")
print(f"Client : {order.customer}")
print(f"Total : {order.total():,} VND")
Le fichier src/shopcart/__init__.py peut être vide.
4.4. Installation en mode editable
python -m pip install -e ".[dev]"
shopcart
Client : Nguyễn Văn An
Total : 83,000 VND
Le flag -e (editable) lie le package au code source au lieu de le copier dans site-packages. Ainsi, quand vous modifiez order.py, les changements prennent effet immédiatement sans réinstallation. La partie ".[dev]" signifie installer le projet du répertoire courant avec le groupe dev, donc pytest et ruff sont aussi installés. La commande shopcart est disponible dans le terminal grâce à la déclaration [project.scripts].
Remarque : Certains shells comme zsh interprètent
[...]comme un glob pattern. Donc vous devez toujours mettre".[dev]"entre guillemets.
5. Écrire et exécuter des tests
# tests/test_order.py
from decimal import Decimal
import pytest
from shopcart.order import Order
def test_total_sums_all_lines():
order = Order(customer="Trần Thị Bình")
order.add("Thé pêche", "35000", 2)
order.add("Flan", "15000", 3)
assert order.total() == Decimal("115000")
def test_empty_order_has_zero_total():
assert Order(customer="Lê Minh").total() == Decimal("0")
def test_add_rejects_non_positive_quantity():
order = Order(customer="Phạm Hà")
with pytest.raises(ValueError, match="quantité"):
order.add("Eau minérale", "10000", 0)
python -m pytest -q
... [100%]
3 passed in 0.02s
Le test importe shopcart.order comme un package installé. C’est pourquoi vous avez besoin de l’étape pip install -e ci-dessus. Sans cela, le src layout donnera une erreur ModuleNotFoundError, et c’est intentionnel : cela vous force à tester ce que les utilisateurs vont installer. Le troisième test utilise pytest.raises pour vérifier qu’une quantité invalide est rejetée avec un message contenant « quantité ».
6. Verrouiller les versions : pyproject.toml ou requirements.txt ?
Ces deux fichiers ne s’excluent pas mutuellement. Ils servent deux objectifs différents :
pyproject.toml |
Fichier lock (locked requirements.txt) |
|
|---|---|---|
| Objectif | Décrire ce que le projet a besoin | Reproduire précisément un environnement |
| Versions | Plages (>=2.31,<3) |
Fixes (==2.32.3) |
| Utilisé pour | Bibliothèques et applications | Applications déployées (serveurs, Docker) |
Avec une bibliothèque destinée à être publiée sur PyPI, vous n’avez besoin que de pyproject.toml. Si vous verrouillez les versions, les utilisateurs de votre bibliothèque rencontreront des conflits.
Avec une application s’exécutant sur un serveur, vous devez avoir un fichier lock pour que chaque déploiement installe exactement le même ensemble de versions :
python -m pip install -e .
python -m pip freeze --exclude-editable > requirements.lock
Lors du déploiement, installez d’abord à partir du fichier lock, puis installez le projet, pour empêcher pip d’augmenter automatiquement les versions des dépendances :
python -m pip install -r requirements.lock
python -m pip install --no-deps .
Si vous voulez un processus de lock plus élégant, vous pouvez utiliser pip-tools (commande pip-compile pyproject.toml) ou uv. Les deux lisent directement pyproject.toml et génèrent un fichier lock avec des hashs.
7. Erreurs courantes
- Installer des packages avec
sudo pip install. Cette commande écrase les packages du système et peut casser les outils système. De nombreuses distributions Linux modernes bloquent explicitement cela avec une erreurexternally-managed-environment. La solution est d’utiliser toujours un venv. - Oublier d’activer le venv avant d’installer ou d’exécuter. Vous pouvez vérifier avec
which pythoncomme dans la section 2.2, ou appeler directement.venv/bin/python -m pip .... - Créer un venv avec un Python et l’exécuter avec un autre. Le venv est lié à l’interpréteur qui l’a créé. Quand vous mettez à niveau Python (par exemple de 3.11 à 3.12), supprimez
.venvet recréez-le. - Déplacer ou renommer le répertoire du projet. Les scripts dans
.venv/bincontiennent des chemins absolus, donc ils cassent après le déplacement. Vous devez recréer le venv. - Mélanger l’ancien
setup.pyavecpyproject.tomlet déclarer des dépendances dans les deux. Choisissez une seule source de vérité. Pour les nouveaux projets, c’est la section[project]. - Mettre les outils de dev dans
dependencies.pytestouruffdoivent être dansoptional-dependencies(ou[dependency-groups]si votre outil le supporte), pour que les utilisateurs finaux n’aient pas à les installer.
Remarque : N’utilisez pas un venv quand vous voulez simplement exécuter un outil CLI autonome comme
blackouhttpieà l’échelle de la machine. Dans ce cas,pipxest plus approprié : il crée automatiquement un venv séparé pour chaque outil et ajoute la commande àPATH.
8. Conclusion
Cet article a couvert les trois outils fondamentaux d’un projet Python :
python -m venv .venvisole les dépendances pour chaque projet.python -m pipinstalle les packages sur le bon interpréteur.pyproject.tomldéclare les métadonnées, les dépendances, les groupes optionnels, les commandes CLI et la configuration des outils dans un seul fichier.
En combinant le src layout, pip install -e ".[dev]" et pytest, vous avez un projet propre, facile à tester et à partager. Pour une application nécessitant un déploiement stable, ajoutez un fichier lock à côté de pyproject.toml pour que chaque environnement installe exactement le même ensemble de versions.