Code is Science is back: the journal code policy tracker now covers 24 journals.See the tracker

Reproducible environments for research code

Sharing your code is step one. Making sure it still runs on someone else's machine, next year, is step two. The tools are mature. The hard part is choosing the right level of effort.

PracticeBy the Code is Science editorsOct 1, 20267 min read
Key takeaways
  • A lockfile that pins exact versions is the cheapest big win for reproducibility.
  • Containers capture the whole system, at a cost in size and complexity. Use them when the system matters.
  • Notebooks are fine, if you restart and run all before you share them.

Research code rarely breaks because the science changed. It breaks because a dependency changed. A package renames a function, a default parameter shifts, a compiled library stops building on a new operating system. The code is the same, the result is not, or there is no result at all.

Capturing your computational environment is how you protect against that. There is a ladder of options, from a one line text file to a full container image. Climb only as high as your project needs.

Why does "it works on my machine" happen?

Your machine has a specific language version, a specific set of packages at specific versions, system libraries, environment variables and sometimes files in places your code quietly assumes. Someone else's machine differs in some of those ways. Most of the time the differences do not matter. When they do, the failures are often silent: the code runs and produces slightly different numbers.

The goal is to write down enough of your environment that someone else can rebuild it.

Level 1: list your dependencies

At minimum, write down what you installed. In Python, a requirements.txt. In R, a DESCRIPTION file or a list in the README. Name the language version too.

This is better than nothing, but a bare list of package names will install whatever is newest on the day someone runs it. That may not be what you used.

Level 2: pin versions with a lockfile

A lockfile records the exact version of every package, including the dependencies of your dependencies. It is the biggest reproducibility gain for the least effort.

  • Python: tools such as pip-tools, Poetry and uv generate lockfiles from a short list of top level requirements.
  • Conda: conda environments are described in environment.yml. Exporting with exact versions, or using conda-lock, pins them. Conda also manages non-Python dependencies, which helps with compiled scientific libraries.
  • R: renv creates a project specific library and a renv.lock file. Collaborators run one command to restore the same package versions.
  • Julia: Project.toml and Manifest.toml do the same job built in.

Commit the lockfile to the repository and update it deliberately, not by accident.

# R, inside the project
renv::init()       # create a project library
renv::snapshot()   # write renv.lock
# a collaborator later runs:
renv::restore()

Level 3: containers

A container packages your code with an entire operating system userland: system libraries, language runtime and packages. Anyone with a container runtime gets the same environment.

  • Docker is the most widely used tool for building and running containers.
  • Apptainer, formerly Singularity, is designed for shared HPC systems where users do not have administrator rights. It can run images built with Docker.

Containers are the right choice when your analysis depends on system level software, compiled tools, specific GPU libraries or a complex mix of languages. They cost more: images are large, builds take time and you need to learn another tool.

Two habits make containers more reproducible. Pin the base image to a specific version or digest rather than latest, and install packages from your lockfile inside the build. Also archive the built image or the full build recipe, because rebuilding the same Dockerfile years later can fetch different software.

Level 4: run it in the browser

Binder builds an environment from a public repository and opens it in the browser, so readers can run your notebooks without installing anything. It reads standard files such as requirements.txt, environment.yml or R installation scripts, which means a good lockfile already gets you most of the way. The public mybinder.org service is free and has limited resources and session time, so it works best for small demonstrations rather than full analyses.

Commercial platforms such as Code Ocean offer a similar experience with persistent compute capsules, and some journals have partnered with them for code review.

What goes wrong with notebooks?

Notebooks are great for exploration and explanation. They also have well known traps:

  • Hidden state. Cells can be run in any order. A variable defined in a cell you later deleted can still live in memory, so the notebook works for you and fails for everyone else.
  • Out of order execution. Execution counts that jump around are a warning sign.
  • Stored outputs. Outputs saved in the file may not match what the current code produces.
  • Hard to diff and review. Notebook files mix code, outputs and metadata, which makes version control noisy.

The fix is simple and non negotiable: before sharing, restart the kernel and run all cells top to bottom. If it does not finish cleanly, it is not ready. For anything long running or reused, move the logic into plain scripts or modules and keep the notebook as a thin layer that calls them.

How much is enough?

Match the effort to the stakes and the stack:

  • A small analysis in a single language: a lockfile and a clear README.
  • Heavy compiled dependencies or mixed languages: a lockfile plus a container definition.
  • Teaching material or a demonstration figure: a lockfile that Binder can read.
  • Long running pipelines on HPC: Apptainer images plus a workflow manager.

The Good Enough Practices paper makes the same point in its own way: do what is realistic and what you will actually maintain. A pinned environment you keep up to date beats an elaborate setup you abandon.

Test it on a clean machine

Whatever you choose, prove it works. Clone the repository on a fresh machine or in a CI job, build the environment from your files alone and run the analysis. Many hosting platforms provide free continuous integration that can do this on every push. If it passes there, it has a real chance of passing for your readers.

Questions

Is a requirements.txt file enough for reproducibility?

It is a start, but without exact versions it installs whatever is newest. Pin versions with a lockfile so others get the same packages you used.

Do I need Docker for my research code?

Not always. A lockfile covers most single language analyses. Containers help when your work depends on system libraries, compiled tools or several languages.

Can I share Jupyter notebooks as my reproducible code?

Yes, if you restart the kernel and run all cells before sharing, and pair the notebook with a pinned environment. For heavy logic, keep code in scripts that the notebook calls.

Keep reading. More from the desk.