Leçon 3 sur 8
Unité · La même réponse deux fois
Ça marche sur ma machine, et c'est le rapport de bogue
Le même script, les mêmes données et une autre version de pandas produisent d'autres nombres. Un fichier de verrouillage est ce qui transforme « ça marche sur ma machine » d'une défense en une affirmation vérifiable, et cela coûte une commande.
Ce que l’ordinateur d’un collègue fait différemment
Vous envoyez un script et un CSV. Il l’exécute. Le nombre diffère.
| Ce qui diffère | Ce que cela change |
|---|---|
| Version de paquet | Un argument par défaut, une règle d’arrondi, un ordre de tri |
| Version du langage | Ordre des dictionnaires, division entière, traitement des chaînes |
| Système d’exploitation | Fins de ligne, ordre des fichiers, séparateurs de chemin, locale |
| Locale | 1,234 interprété comme 1234 ou comme 1,234 |
| Ce qui est déjà installé | Une bibliothèque que votre script importe sans jamais la déclarer |
Rien de tout cela n’est dans votre dépôt, raison pour laquelle un code identique ne suffit pas. Un fichier de verrouillage est la moitié manquante de l’analyse.
Python : uv
uv init # creates pyproject.toml
uv add pandas matplotlib # records the dependency and resolves it
uv run python run.py # runs inside the pinned environment
Deux fichiers, et les deux sont commités. pyproject.toml dit ce que vous avez
demandé — pandas>=2.0 — et uv.lock dit exactement ce que vous avez obtenu, jusqu’à
l’empreinte de chaque paquet de l’arbre.
Un collègue lance uv sync et dispose de votre environnement, sur son système,
sans que vous sachiez ce qu’il avait installé auparavant.
Figez aussi la version du langage.
# pyproject.toml
requires-python = ">=3.12,<3.13"
Une plage de versions, non une version unique. Figer à 3.12.4 exactement
signifie qu’un collègue en 3.12.7 ne peut pas l’exécuter du tout, ce qui est un
échec pire que celui qu’on prévenait.
R : renv
renv::init() # snapshots the project library
renv::snapshot() # after adding a package
renv::restore() # on the colleague's machine
renv.lock est l’artefact équivalent et il est commité. Il enregistre chaque
paquet, sa version et le dépôt d’où il vient, et renv::restore() le reconstruit.
Enregistrez la version de R, ce que renv fait automatiquement, et vérifiez-la
dans le script là où une différence compterait.
if (getRversion() < "4.3.0") stop("This analysis requires R >= 4.3.0")
# The Python equivalent, at the top of the entry point.
import sys
assert sys.version_info >= (3, 12), "This analysis requires Python 3.12+"
Pourquoi un fichier de verrouillage et non requirements.txt
pandas>=2.0
matplotlib
Ce fichier ne dit presque rien. Il se résout en versions différentes selon les
jours et ne mentionne pas les cinquante paquets que ces deux-là entraînent. Une
installation en mars et une en juillet depuis le même requirements.txt sont deux
environnements différents.
| Déclare | Reproduit | |
|---|---|---|
requirements.txt |
L’intention | Non |
requirements.txt avec des == |
Une couche | En partie — les dépendances transitives flottent |
uv.lock / renv.lock |
L’arbre entier | Oui |
Commitez l’intention et le verrou. Le fichier d’intention est ce qu’un humain édite ; le verrou est ce depuis quoi une machine reproduit, et aucun ne remplace l’autre.
Ce que cette plateforme fige, et pourquoi un verrou est inhabituel
{
"packageManager": "pnpm@11.9.0",
"engines": { "node": ">=22" },
"devDependencies": { "typescript": "^6.0.3" }
}
Le gestionnaire de paquets lui-même est figé, parce qu’un pnpm différent résout le fichier de verrouillage différemment, et c’est la couche en dessous de celle que la plupart des projets figent.
TypeScript est maintenu en 6.x délibérément. TypeScript 7 — le compilateur natif —
n’expose pas encore l’API programmatique dont dépend astro check, si bien que
pnpm typecheck échoue purement et simplement en 7. Le verrou a une raison, la
raison est écrite, et elle nomme la condition de sa levée.
C’est la forme que devrait avoir un verrou. Une contrainte de version sans commentaire est une contrainte que personne n’osera retirer et que personne ne peut justifier de garder.
# pandas is pinned below 3.0 because the copy-on-write default changes the
# behaviour of the recode in src/clean.py:88. Revisit when that is rewritten.
pandas = ">=2.1,<3.0"
Les conteneurs, et quand ils en valent la peine
Un conteneur fige aussi le système d’exploitation, la seule couche qu’un fichier de verrouillage ne peut pas atteindre.
Ils en valent la peine quand l’analyse a des dépendances hors Python ou hors R — GDAL, une distribution TeX, un client de base de données —, ou quand elle doit tourner sans surveillance sur un serveur, ou quand le constat sera réexaminé des années plus tard.
Ils n’en valent pas la peine quand un fichier de verrouillage reproduit déjà le résultat et que le public est deux collègues avec des ordinateurs portables. Un conteneur ajoute une étape de construction, un registre et une compétence qu’une petite équipe de suivi-évaluation peut ne pas avoir, et le coût est réel.
FROM python:3.12-slim
COPY pyproject.toml uv.lock ./
RUN pip install uv && uv sync --frozen
COPY . .
CMD ["uv", "run", "python", "run.py"]
Commencez par le fichier de verrouillage. Passez au conteneur quand vous pouvez nommer la dépendance qu’il fige et que le verrou ne peut pas.
Le test qui le prouve
Reproduisez votre propre résultat sur une machine qui n’a jamais vu le projet. Un clone neuf dans un répertoire temporaire en fait l’essentiel.
git clone <repo> /tmp/check && cd /tmp/check
uv sync
uv run python run.py
diff -r outputs/ ~/project/outputs/
# renv::restore() then source("run.R"), and compare.
Si vous ne pouvez pas le faire, « c’est reproductible » n’est pas testé. L’ordinateur d’un collègue vaut mieux et un exécuteur d’intégration continue vaut mieux encore, parce que c’est une machine propre à chaque fois et qu’il s’exécute que quiconque y pense ou non.
Rapportez-le en entier
Computational environment
Python 3.12, dependencies pinned in uv.lock (committed). Reproduce with:
uv sync && uv run python run.py
pandas is held below 3.0 because the copy-on-write default changes the
recode in src/clean.py; this is revisited when that function is rewritten.
The pipeline runs on every push in CI on a clean machine, so the claim
that a fresh checkout reproduces these outputs is tested rather than
asserted.
Analysis run on 2026-03-14 with commit a3f9c21.
La dernière ligne est celle qui rend le reste utilisable un an plus tard. Un résultat sans le commit qui l’a produit se reproduit approximativement ; avec lui, exactement.
La suite
Un environnement figé produit encore une réponse différente à la seconde exécution si le code lit l’horloge, tire un nombre aléatoire, ou se fie à l’ordre dans lequel les fichiers reviennent. La leçon suivante trouve les quatre.