cassionData Analysis

Lesson 3 of 8

Unit · The same answer twice

It works on my machine, and that is the bug report

The same script, the same data and a different pandas version produce different numbers. A lock file is what turns "it works on my machine" from a defence into a testable claim, and it costs one command to make.

PythonR135 minOECD DAC evaluation criteriaCore Humanitarian Standard (CHS)

What a colleague’s laptop does differently

You send a script and a CSV. They run it. The number differs.

What differs What it changes
Package version A default argument, a rounding rule, a sort order
Language version Dictionary ordering, integer division, string handling
Operating system Line endings, file ordering, path separators, locale
Locale 1,234 parsed as 1234 or as 1.234
What is already installed A library your script imports and never declares

None of those is in your repository, which is why the code being identical is not enough. A lock file is the missing half of the analysis.

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

Two files, and both are committed. pyproject.toml says what you asked for — pandas>=2.0 — and uv.lock says exactly what you got, down to the hash of every package in the tree.

A colleague runs uv sync and has your environment, on their operating system, without you knowing what they had installed before.

Pin the language version too.

# pyproject.toml
requires-python = ">=3.12,<3.13"

A version range, not a single version. Pinning to 3.12.4 exactly means a colleague on 3.12.7 cannot run it at all, which is a worse failure than the one you were preventing.

R: renv

renv::init()                 # snapshots the project library
renv::snapshot()             # after adding a package
renv::restore()              # on the colleague's machine

renv.lock is the equivalent artefact and is committed. It records every package, its version, and the repository it came from, and renv::restore() rebuilds it.

Record the R version, which renv does automatically, and check it in the script where a difference would matter.

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+"

Why a lock file and not requirements.txt

pandas>=2.0
matplotlib

That file says almost nothing. It resolves to different versions on different days and does not mention the fifty packages those two pull in. A build in March and a build in July from the same requirements.txt are different environments.

Declares Reproduces
requirements.txt Intent No
requirements.txt with == pins One layer Partly — transitive dependencies float
uv.lock / renv.lock The whole tree Yes

Commit both the intent and the lock. The intent file is what a human edits; the lock is what a machine reproduces from, and neither replaces the other.

What this platform pins, and why one pin is unusual

{
  "packageManager": "pnpm@11.9.0",
  "engines": { "node": ">=22" },
  "devDependencies": { "typescript": "^6.0.3" }
}

The package manager itself is pinned, because a different pnpm resolves the lockfile differently and that is the layer below the layer most projects pin.

TypeScript is held at 6.x deliberately. TypeScript 7 — the native compiler — does not yet expose the programmatic API that astro check depends on, so pnpm typecheck fails outright on 7. The pin has a reason, the reason is written down, and it names the condition for removing it.

That is the shape a pin should have. A version constraint with no comment is a constraint nobody will dare remove and nobody can justify keeping.

# 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"

Containers, and when they are worth it

A container pins the operating system as well, which is the one layer a lock file cannot reach.

Worth it when the analysis has non-Python or non-R dependencies — GDAL, a TeX distribution, a database client — or when it has to run unattended on a server, or when the finding will be re-examined years later.

Not worth it when a lock file already reproduces the result and the audience is two colleagues with laptops. A container adds a build step, a registry and a skill that a small M&E team may not have, and the cost is real.

FROM python:3.12-slim
COPY pyproject.toml uv.lock ./
RUN pip install uv && uv sync --frozen
COPY . .
CMD ["uv", "run", "python", "run.py"]

Start with the lock file. Reach for a container when you can name the dependency it is pinning that the lock file cannot.

The test that proves it

Reproduce your own result on a machine that has never seen the project. A fresh clone in a temporary directory is most of the way there.

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.

If you cannot do that, “it is reproducible” is untested. A colleague’s laptop is better and a CI runner is better still, because it is a clean machine every time and it runs whether or not anyone remembers to.

Report it whole

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.

The last line is the one that makes the rest usable a year later. A result without the commit that produced it can be approximately reproduced; with it, exactly.

What comes next

A pinned environment still produces a different answer on a second run if the code reads the clock, draws a random number, or trusts the order files come back in. The next lesson finds all four.

Teach this lesson

The lesson as a slide deck, with the prose kept in the speaker notes rather than on the slide. Generated from this page, so it cannot fall out of step with it.

Start the slideshowRead the slides

The PDF needs no software and projects from any machine. The PowerPoint file is there to be edited — add your organisation's branding, cut a section for a shorter session, or merge two lessons into a workshop.