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 .venv sur 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 pip met à jour pip lui-même. Les anciennes versions de pip ne lisent pas toujours correctement pyproject.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. show affiche 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 par hatchling ou flit_core sans modifier le reste.
  • [project] contient les métadonnées standard. requires-python empêche l’installation sur des versions Python non supportées.
  • dependencies liste 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 groupe dev contient les outils de développement que l’utilisateur final n’a pas besoin d’avoir.
  • [project.scripts] crée une commande shopcart dans le terminal. Cette commande appelle la fonction main du module shopcart.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 erreur externally-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 python comme 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 .venv et recréez-le.
  • Déplacer ou renommer le répertoire du projet. Les scripts dans .venv/bin contiennent des chemins absolus, donc ils cassent après le déplacement. Vous devez recréer le venv.
  • Mélanger l’ancien setup.py avec pyproject.toml et 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. pytest ou ruff doivent être dans optional-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 black ou httpie à l’échelle de la machine. Dans ce cas, pipx est 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 .venv isole les dépendances pour chaque projet.
  • python -m pip installe les packages sur le bon interpréteur.
  • pyproject.toml dé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.

pythonpipvenvpyprojectpackaging