cassionData Analysis

Back to the lessonLesson 3 of 8The same answer twice

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

The same deck as the downloads, rendered as a page. Start the slideshow to present it full screen — arrow keys or a click advance one slide, Escape leaves.

Slides · PDFSlides · PowerPoint

  1. Slide 1 / 29

    What this lesson covers

    • What a colleague's laptop does differently
    • Python: uv
    • R: renv
    • Why a lock file and not requirements.txt
    • What this platform pins, and why one pin is unusual
    • Containers, and when they are worth it
    • The test that proves it
    • Report it whole
    • What comes next
    Speaker notes
    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.
  2. Slide 2 / 29

    What a colleague's laptop does differently

    What differsWhat it changes
    Package versionA default argument, a rounding rule, a sort order
    Language versionDictionary ordering, integer division, string handling
    Operating systemLine endings, file ordering, path separators, locale
    Locale1,234 parsed as 1234 or as 1.234
    What is already installedA library your script imports and never declares
    Speaker notes
    You send a script and a CSV. They run it. The number differs.
  3. Slide 3 / 29

    What a colleague's laptop does differently

    • None of those is in your repository — which is why the code being identical is not enough
    Speaker notes
    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.
  4. Slide 4 / 29

    Python: uv — Shell

    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
  5. Slide 5 / 29

    Python: uv

    • Two files, and both are committed — pyproject.toml says what you asked for — pandas>=2.0 — and uv.lock says…
    • A colleague runs uv sync and has your environment — on their operating system, without you knowing what they had…
    • Pin the language version too
    Speaker notes
    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.
  6. Slide 6 / 29

    Python: uv — Example

    # pyproject.toml
    requires-python = ">=3.12,<3.13"
  7. Slide 7 / 29

    Python: uv

    • 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,…
    Speaker notes
    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.
  8. Slide 8 / 29

    R: renv — In R

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

    R: renv

    • renv.lock is the equivalent artefact and is committed — It records every package, its version, and the repository it…
    • Record the R version, which renv does automatically — and check it in the script where a difference would matter
    Speaker notes
    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.
  10. Slide 10 / 29

    R: renv — In R

    if (getRversion() < "4.3.0") stop("This analysis requires R >= 4.3.0")
  11. Slide 11 / 29

    R: renv — In Python

    # The Python equivalent, at the top of the entry point.
    import sys
    assert sys.version_info >= (3, 12), "This analysis requires Python 3.12+"
  12. Slide 12 / 29

    Why a lock file and not requirements.txt — Example

    pandas>=2.0
    matplotlib
  13. Slide 13 / 29

    Why a lock file and not requirements.txt

    • That file says almost nothing — It resolves to different versions on different days and does not mention the fifty…
    Speaker notes
    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.
  14. Slide 14 / 29

    Why a lock file and not requirements.txt

    DeclaresReproduces
    requirements.txtIntentNo
    requirements.txt with == pinsOne layerPartly — transitive dependencies float
    uv.lock / renv.lockThe whole treeYes
  15. Slide 15 / 29

    Why a lock file and not requirements.txt

    • Commit both the intent and the lock — The intent file is what a human edits; the lock is what a machine reproduces…
    Speaker notes
    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.
  16. Slide 16 / 29

    What this platform pins, and why one pin is unusual — Example

    {
      "packageManager": "pnpm@11.9.0",
      "engines": { "node": ">=22" },
      "devDependencies": { "typescript": "^6.0.3" }
    }
  17. Slide 17 / 29

    What this platform pins, and why one pin is unusual

    • The package manager itself is pinned — because a different pnpm resolves the lockfile differently and that is the layer…
    • TypeScript is held at 6.x deliberately — TypeScript 7 — the native compiler — does not yet expose the programmatic API…
    • That is the shape a pin should have — A version constraint with no comment is a constraint nobody will dare remove and…
    Speaker notes
    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.
  18. Slide 18 / 29

    What this platform pins, and why one pin is unusual — Example

    # 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"
  19. Slide 19 / 29

    Containers, and when they are worth it

    • Worth it when — the analysis has non-Python or non-R dependencies — GDAL, a TeX distribution, a database client — or…
    • Not worth it when — a lock file already reproduces the result and the audience is two colleagues with laptops
    Speaker notes
    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.
  20. Slide 20 / 29

    Containers, and when they are worth it — Example

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

    Containers, and when they are worth it

    • Start with the lock file — Reach for a container when you can name the dependency it is pinning that the lock file…
    Speaker notes
    Start with the lock file. Reach for a container when you can name the dependency it is pinning that the lock file cannot.
  22. Slide 22 / 29

    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…
    Speaker notes
    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.
  23. Slide 23 / 29

    The test that proves it — Shell

    git clone <repo> /tmp/check && cd /tmp/check
    uv sync
    uv run python run.py
    diff -r outputs/ ~/project/outputs/
  24. Slide 24 / 29

    The test that proves it — In R

    # renv::restore() then source("run.R"), and compare.
  25. Slide 25 / 29

    The test that proves it

    • If you cannot do that, "it is reproducible" is untested — A colleague's laptop is better and a CI runner is better…
    Speaker notes
    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.
  26. Slide 26 / 29

    Report it whole — Example

    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.
  27. Slide 27 / 29

    Report it whole

    • The last line is the one that makes the rest usable a year later — A result without the commit that produced it can be…
    Speaker notes
    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.
  28. Slide 28 / 29

    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.
    Speaker notes
    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.
  29. Slide 29 / 29

    Where this goes next

    Read the full lesson, with runnable code Back to the lesson