# Dependency Pinning for Reproducible Research

By the end of this article you will be able to record the exact versions of every software package your analysis depends on, rebuild that environment on a new machine or a new cluster, package the result in a container or a workflow system, and share it so that another scientist can rerun your analysis and get the same numbers. You will also be able to audit an inherited project and identify which dependencies are missing or floating.

You will get the most from this walkthrough if you have the following on hand:

- A computer with Python 3.11 or newer installed, ideally through Miniforge or Anaconda
- Git for version control
- Optional but recommended, Docker or Apptainer for containers
- Optional, R 4.3 or newer with the renv package if you work in R
- A small analysis project of your own to practice on

## What Dependency Pinning Means

A dependency is any external piece of software your code relies on to run. For a typical life science project that list includes the language interpreter (Python, R, Julia), the packages you import directly (pandas, scikit-learn, ggplot2, Bioconductor packages), the packages those packages import (transitive dependencies), system libraries (BLAS, HDF5, libxml2), command line tools (samtools, FASTQC), and reference data or model files with their own versions.

Dependency pinning means recording an exact version for each of these, so that `numpy 1.26.4` stays `numpy 1.26.4` no matter when or where the code runs. The alternative is a floating dependency, written as `numpy>=1.20` or simply `import numpy`, where the version installed at runtime depends on whatever happens to be current that day.

The stakes are concrete. Software ecosystems in biology are fragmented, and researchers routinely need to integrate tools written in different languages with conflicting requirements. This situation is well known enough in bio-image analysis that practitioners describe it as "dependency hell," and dedicated environment management systems have been built specifically to resolve it [1]. When versions drift, a pipeline that validated a clinical variant call last year can produce different annotations this year, even with the same input file.

The incentive structure does not always help. A survey of research data policies across 171 materials science journals found that while nearly all journals now have a data policy, expectations for research software lag behind expectations for data, with limited attention to versioning, dependency disclosure, and reproducible execution environments [2]. In other words, you often cannot count on a journal checklist to force you to pin dependencies. You have to do it yourself, before the analysis, not after the review.

The cost of skipping pinning shows up most sharply when you inherit someone else's project. Trainees in [computational biology](/knowledge/bioinformatics/vaccinomics-and-the-future-of-personalized-vaccines) frequently inherit research plans whose datasets, analytical strategies, and technical constraints were defined before their arrival, and these plans rarely specify the dependencies needed for reliable execution [3]. A well-pinned repository turns that reconstruction problem into a one-line install command.

## Dependency Pinning for Reproducible Research, Step by Step

![Seven-step flow from inventorying installed packages through lock files, containers, workflow pins, data versions and archiving the environment.](/article-images/illustrations/dependency-pinning-reproducible-research-pinning-steps.webp)

This walkthrough builds a pinned environment for a small Python analysis, then scales the same logic to containers, workflow managers, R, and data versioning. Each step shows the command and the output you should expect to see.

### Step 1. Inventory the environment you have now

Before you pin anything, record what is actually installed. If you use conda, run:

```bash
conda list
```

Expected output (abbreviated):

```
# packages in environment at /home/you/miniforge3/envs/analysis:
#
# Name      Version   Build       Channel
biopython   1.83      py311h...   conda-forge
numpy       1.26.4    py311h...   conda-forge
pandas      2.2.2     py311h...   conda-forge
python      3.11.8    h...        conda-forge
scipy       1.12.0    py311h...   conda-forge
```

If you use plain pip inside a virtual environment, run:

```bash
pip freeze
```

Expected output:

```
biopython==1.83
numpy==1.26.4
pandas==2.2.2
scipy==1.12.0
```

Two details matter here. The `==` in pip output is an exact version pin. The build string in conda output (the `py311h...` column) encodes the Python version and low-level build details, which is why conda exports are more complete than pip freezes for the same environment.

### Step 2. Write a pinned environment file

Create a file named `environment.yml` in your project root. The `name` line sets the environment name, the `channels` lines control where packages come from, and every entry in `dependencies` is pinned to an exact version.

```yaml
name: aggregation-analysis
channels:
  - conda-forge
dependencies:
  - python=3.11.8
  - numpy=1.26.4
  - pandas=2.2.2
  - scipy=1.12.0
  - biopython=1.83
```

Anyone with this file can recreate the environment:

```bash
conda env create -f environment.yml
conda activate aggregation-analysis
```

Expected output:

```
Collecting package metadata: done
Solving environment: done
Downloading and Extracting Packages
Preparing transaction: done
Verifying transaction: done
Executing transaction: done
To activate this environment, use: conda activate aggregation-analysis
```

For a pip-based project, the equivalent file is `requirements.txt`, with one exact pin per line:

```
numpy==1.26.4
pandas==2.2.2
scipy==1.12.0
biopython==1.83
```

Install it with:

```bash
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
```

A pinned environment file like this is a core component of FAIR software. A published Julia-based agent-based model of protein aggregation, designed as a FAIR research software object, explicitly ships version-pinned dependencies alongside centralized parameters and machine-readable metadata so that independent researchers can rerun the simulations [4].

### Step 3. Pin the transitive dependencies with a lock file

Your `requirements.txt` pins the packages you know about. Your analysis also depends on the packages those packages depend on, and those are the ones that drift silently. A lock file pins every package in the full dependency tree, resolved at one point in time.

A widely used tool for this is pip-tools. You write a short `requirements.in` file with the packages you care about, optionally with version ranges:

```
pandas
scipy<1.13
```

Then compile it:

```bash
pip install pip-tools
pip-compile requirements.in
```

Expected output (abbreviated):

```
#
# This file is autogenerated by pip-compile with Python 3.11
# by the following command:
#
#    pip-compile requirements.in
#
numpy==1.26.4
    via scipy==1.12.0
pandas==2.2.2
python-dateutil==2.9.0.post0
    via pandas==2.2.2
pytz==2024.1
    via pandas==2.2.2
scipy==1.12.0
six==1.16.0
    via python-dateutil==2.9.0.post0
```

The generated `requirements.txt` now pins pandas, scipy, and every package they pull in, including python-dateutil and pytz, which you never would have listed by hand. Commit both files to Git. The `.in` file documents your intent, the compiled file documents the exact resolved state.

### Step 4. Pin the interpreter and system libraries in a container

A lock file pins the Python packages. It does not pin the operating system, the C compiler that built numpy, or the system BLAS library. A container pins all of it. Software containerization packages code, dependencies, and the execution environment into a single portable unit, and it has become a cornerstone of modern computational biology precisely because it supports reproducibility across heterogeneous systems [5].

A minimal Dockerfile for the environment above:

```dockerfile
FROM python:3.11.8-slim

WORKDIR /work
COPY requirements.txt .
RUN pip install -r requirements.txt

COPY analysis.py .
CMD ["python", "analysis.py"]
```

Notice that the base image is pinned twice. The tag `3.11.8-slim` fixes the Python version and OS variant. For maximum reproducibility, pin the image digest, which is an irreversible content hash:

```dockerfile
FROM python@sha256:4b01c9b0f2471e5b2b0f1f0a9c7e6d5a8f3b2c1d9e0f7a6b5c4d3e2f1a0b9c8d
```

Build and run:

```bash
docker build -t aggregation-analysis:1.0 .
docker run aggregation-analysis:1.0
```

Expected output:

```
[+] Building 12.3s (9/9) FINISHED
 => exporting to image
 => => naming to docker.io/library/aggregation-analysis:1.0
Runs complete. Results written to results/
```

On high-performance computing clusters where users cannot run Docker, Apptainer serves the same role:

```bash
apptainer pull aggregation-analysis.sif docker://aggregation-analysis:1.0
apptainer exec aggregation-analysis.sif python analysis.py
```

One caution from the containerization literature itself. Containers are powerful, and superficial use can undermine their benefits, producing brittle environments or false confidence in reproducibility. The practical guidance is to treat containerization as a set of lifecycle decisions, including when to containerize, how to balance reproducibility with flexibility, and how to manage dependencies and data inside the image [5]. A container that installs packages with `pip install numpy` at build time without pins is not reproducible. It is a frozen snapshot of one lucky build.

### Step 5. Pin tools inside a workflow manager

Once an analysis grows beyond a single script, a workflow manager coordinates the steps, and each step can carry its own pinned container. Nextflow is a common choice in the life sciences. A proteomics pipeline built on Nextflow encapsulates FragPipe, automates workflow generation, manages tool dependencies, and runs reproducibly on HPC, cloud, and cluster environments, cutting runtime nearly in half on a typical 58 GB DIA dataset compared with other workflow-based solutions [6]. A genomic variant annotation pipeline called MuSA follows the same pattern, standardizing inputs and executing parallel annotation branches in a fully containerized nf-core-compliant Nextflow workflow [7].

In a Nextflow pipeline, the pin lives in the process definition:

```groovy
process ANNOTATE {
    container 'biocontainers/samtools:1.19.2'

    input:
    path bam

    script:
    """
    samtools view -b ${bam} > filtered.bam
    """
}
```

The `container` directive ties that one process to an exact tool version, independent of whatever is installed on the machine running the pipeline. If your project uses Snakemake, the equivalent structure is a rule with a `container:` directive and a Conda environment file per rule. The principle is identical: the version travels with the step, not with the machine.

### Step 6. Pin the data and reference versions

Dependencies are not only code. A reference genome build, a database release, or a curated dataset has versions too, and changing them changes results. DataLad, a Python-based tool built on Git and git-annex, manages code, data, and their relationships together, linking datasets as precisely versioned, lightweight dependencies and capturing complete process provenance so that transformations can be automatically recomputed [8].

In neuroimaging, CuBIDS incorporates DataLad as an optional dependency to ensure reproducibility and provenance tracking throughout the curation of large BIDS datasets, while helping users identify unique combinations of imaging parameters before running processing pipelines on representative subsets [9]. The pattern generalizes: record which data snapshot you used, with a version or a checksum, in the same repository as your pinned code.

### Step 7. Archive the environment and document the hardware

When you publish, archive the environment files and the container itself. The PoLoCo workflow for pooled low-coverage genome assembly made its workflow, documentation, and software environments openly available in Zenodo, and documented the computational requirements of the major components on standard HPC infrastructure as part of the case study [10]. That documentation matters because a pinned environment plus a machine with 16 GB of RAM may still fail on a 400 GB assembly job. Pinning software versions removes one source of variability, and recording resource needs removes another.

For training contexts, the same principle applies. Cloud-based tutorial platforms for RNA-seq analysis deliver reusable, reproducible learning workflows so that researchers at small institutions can run standardized environments without local setup [11]. If the environment is pinned, the tutorial works the same way for every learner.

## How to Check That Your Pinning Actually Works

The test is a clean rebuild. Follow this sequence:

1. Clone your repository into a brand new directory.
2. Build the environment from the pinned files alone, with no packages cached if you can manage it.
3. Run the full analysis on the test input.
4. Compare the output checksums against your original results.

On Linux or macOS, generate a checksum of an output file:

```bash
sha256sum results/aggregation_counts.csv
```

Expected output:

```
a3f8c1e2b9d4f5e6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0  results/aggregation_counts.csv
```

Run the same command on the output produced by the rebuilt environment. If the hashes match, your pin is complete at the level of software. If they differ, the mismatch itself is diagnostic. A different hash with the same pinned versions points to unpinned data, an unpinned system library, or genuinely stochastic code that needs a fixed random seed. All three are findings worth having before a reviewer finds them.

## Choosing a Pinning Level: Options Compared

![Decision path asking whether the analysis will be rerun, shared or published, leading to exact pins, a lock file or a digest-pinned container.](/article-images/illustrations/dependency-pinning-reproducible-research-pinning-level.webp)

Different projects justify different levels of rigor. The table below summarizes the standard options.

| Approach | What it records | Best for | Main weakness |
|--|--|--|--|
| No pinning | Nothing | Throwaway exploration | Results cannot be reproduced at all |
| Loose range (`numpy>=1.20`) | A floor version | Shared libraries, not analyses | New versions change behavior silently |
| Exact direct pins (`numpy==1.26.4`) | Your direct imports | Short scripts, tutorials | Transitive dependencies still float |
| Lock file (pip-tools, conda-lock) | Full resolved dependency tree | Standard analyses | OS and system libraries unpinned |
| Container with tagged image | OS, libraries, packages, interpreter | Pipelines, shared analyses | Data and host behavior still vary |
| Container pinned by digest | Bit-for-bit image content | Publications, clinical work | Larger images, digest management overhead |
| Container plus data versioning (DataLad) | Software plus data versions and provenance | End-to-end reproducibility | Higher learning curve |

A reasonable rule of thumb: use exact pins or a lock file for anything you might need to rerun, and add a digest-pinned container for anything you plan to publish.

## Dependency Pinning Tips That Pay Off

- Pin at project start, not at submission time. Reconstructing versions after the fact means re-running the analysis and hoping the numbers hold.
- Put the environment files in the repository root, not in a subfolder. People find them there.
- Record the language version explicitly. `python=3.11.8` in the environment file, or `R.version` in an R script, costs one line.
- Use a fixed random seed for stochastic code. Pinning packages does not pin randomness. Set `numpy.random.seed(42)` or the equivalent, and record the seed alongside the parameters.
- Export the environment from the actual working environment, not from memory. `conda env export > environment.yml` captures reality.
- Tag releases. When you tag version 1.0 of the code, rebuild the container with the same tag so the two stay linked.
- Note in your README which file to use first. A one-line install command in the README prevents most rerun failures.

## Common Mistakes and Limitations

**Relying on `:latest` or unpinned base images.** A Dockerfile that starts `FROM python:latest` changes every time Python releases. Pin the tag, and for publications pin the digest.

**Pinning only direct dependencies.** If scipy 1.12.0 works with numpy 1.26.4 today but your lock file is absent, a fresh install may pull a numpy that breaks scipy. Lock files exist to prevent exactly this.

**Forgetting system-level dependencies.** HDF5 versions, BLAS implementations, and graphics libraries can alter numerical output between operating systems. Containers address this, lock files alone do not.

**Treating a container as proof of reproducibility.** A container built from unpinned install commands simply froze one uncontrolled state. The container must be built from pinned inputs to deliver the guarantee.

**Ignoring data and reference versions.** A perfectly pinned pipeline running against an updated reference genome is a new analysis. Version the data alongside the code [8].

**Not testing the rebuild.** Pinning that has never been reinstalled on a clean machine is a hypothesis. Test it before you need it.

**Assuming the journal will catch gaps.** Journal policies for software, including dependency disclosure and reproducible execution environments, remain substantially less developed than policies for data, even in fields with active data-sharing norms [2]. Build the reproducibility checklist into your own workflow.

**Over-pinning shared tools.** If you maintain a library used by others, exact pins in the library itself can create conflicts for your users. Exact pins belong in application and analysis environments. Ranges belong in installable libraries. This distinction is one of the most common points of confusion.

An honest limitation: full computational reproducibility is a spectrum, not a binary. A lock file plus a digest-pinned container plus versioned data gets you close, and for most life science analyses that combination is more than sufficient. Individual projects also differ in the infrastructure available, and where a resource-constrained lab cannot maintain containers, a well-documented lock file with recorded system information is a solid fallback.

## A Reproducibility Checklist

Run this list before you publish or hand a project to a collaborator:

1. Language interpreter version recorded (for example Python 3.11.8, R 4.3.1)
2. All direct package dependencies pinned to exact versions
3. Full dependency tree captured in a lock file
4. Environment files committed to version control at the repository root
5. Container image built from pinned inputs, tagged, and pinned by digest for publications
6. Every workflow step tied to its own pinned container or environment
7. Data files and reference resources versioned or checksummed
8. Random seeds fixed and recorded for stochastic steps
9. Clean-machine rebuild tested and output checksums compared
10. Computational requirements (CPUs, memory, runtime) documented
11. Environment files and container archived to a permanent repository such as Zenodo
12. README states the single command needed to recreate the environment and rerun the analysis

Twelve checked boxes is a defensible position in review, in collaboration, and in your own lab six months from now.

## Frequently Asked Questions

### What does dependency pinning mean in reproducible research?

It means recording the exact version of every piece of software your analysis depends on, including the language interpreter, packages, transitive dependencies, and system libraries, so the environment can be rebuilt identically later. Without pinning, results depend on whatever versions happened to be installed when the code last ran.

### How is a lock file different from a requirements file?

A requirements file lists the packages you directly install, and a lock file captures the entire resolved dependency tree at one moment, including indirect packages and their exact versions. Use the requirements file to state intent and the lock file to guarantee an identical rebuild.

### Do I need to pin every package or only the important ones?

Pin every package in the environment, because the unimportant ones still influence what gets resolved and installed. Modern tools such as pip-compile or conda-lock automate this, so the manual cost is low.

### Should I use conda, pip, or Docker for dependency pinning?

They answer different layers of the problem. Conda and pip pin the language-level packages, and Docker pins the operating system and system libraries as well. Many published pipelines use all three, with conda or pip files inside a container.

### How do I pin dependencies in R?

Use the renv package. Run renv::init() in your project, then renv::snapshot() to write an renv.lock file that records exact package versions and their sources, and renv::restore() to rebuild the environment from that lock file on another machine.

### How do I handle pinning in a Nextflow or Snakemake pipeline?

Give each process or rule its own pinned container image, declared in the process definition. That way each tool travels with its exact version, and the pipeline runs the same way on a laptop, a cluster, or the cloud.

### How often should I update pinned dependencies?

Deliberately and on your schedule, not incidentally. Freeze versions while an analysis is active, and schedule update checkpoints where you upgrade pins, rerun the analysis, and confirm the results before committing the new lock file.

### What do journals and reviewers expect for dependency disclosure?

Expectations vary widely, and surveys of journal policies show that software requirements such as versioning, dependency disclosure, and reproducible execution environments are generally less developed than data-sharing requirements. Providing a pinned environment and a container preempts the question entirely.

<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "FAQPage",
  "mainEntity": [
    {
      "@type": "Question",
      "name": "What does dependency pinning mean in reproducible research?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "It means recording the exact version of every piece of software your analysis depends on, including the language interpreter, packages, transitive dependencies, and system libraries, so the environment can be rebuilt identically later. Without pinning, results depend on whatever versions happened to be installed when the code last ran."
      }
    },
    {
      "@type": "Question",
      "name": "How is a lock file different from a requirements file?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "A requirements file lists the packages you directly install, and a lock file captures the entire resolved dependency tree at one moment, including indirect packages and their exact versions. Use the requirements file to state intent and the lock file to guarantee an identical rebuild."
      }
    },
    {
      "@type": "Question",
      "name": "Do I need to pin every package or only the important ones?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "Pin every package in the environment, because the unimportant ones still influence what gets resolved and installed. Modern tools such as pip-compile or conda-lock automate this, so the manual cost is low."
      }
    },
    {
      "@type": "Question",
      "name": "Should I use conda, pip, or Docker for dependency pinning?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "They answer different layers of the problem. Conda and pip pin the language-level packages, and Docker pins the operating system and system libraries as well. Many published pipelines use all three, with conda or pip files inside a container."
      }
    },
    {
      "@type": "Question",
      "name": "How do I pin dependencies in R?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "Use the renv package. Run renv::init() in your project, then renv::snapshot() to write an renv.lock file that records exact package versions and their sources, and renv::restore() to rebuild the environment from that lock file on another machine."
      }
    },
    {
      "@type": "Question",
      "name": "How do I handle pinning in a Nextflow or Snakemake pipeline?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "Give each process or rule its own pinned container image, declared in the process definition. That way each tool travels with its exact version, and the pipeline runs the same way on a laptop, a cluster, or the cloud."
      }
    },
    {
      "@type": "Question",
      "name": "How often should I update pinned dependencies?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "Deliberately and on your schedule, not incidentally. Freeze versions while an analysis is active, and schedule update checkpoints where you upgrade pins, rerun the analysis, and confirm the results before committing the new lock file."
      }
    },
    {
      "@type": "Question",
      "name": "What do journals and reviewers expect for dependency disclosure?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "Expectations vary widely, and surveys of journal policies show that software requirements such as versioning, dependency disclosure, and reproducible execution environments are generally less developed than data-sharing requirements. Providing a pinned environment and a container preempts the question entirely."
      }
    }
  ]
}
</script>

## Sources

1. [BioImageIT: A novel python-based architecture for reproducible bio-image workflows.](https://pubmed.ncbi.nlm.nih.gov/42405690/)
2. [Journal research data policies in materials science.](https://pubmed.ncbi.nlm.nih.gov/42306234/)
3. [Ten simple rules for executing an inherited research plan in computational biology.](https://pubmed.ncbi.nlm.nih.gov/42284220/)
4. [Reproducible Agent-Based Simulations of Protein Aggregation: A FAIR Implementation.](https://pubmed.ncbi.nlm.nih.gov/42762374/)
5. [Nine quick tips for software containerization.](https://pubmed.ncbi.nlm.nih.gov/42030305/)
6. [Frag'n'Flow: automated workflow for large-scale quantitative proteomics in high performance computing environments.](https://pubmed.ncbi.nlm.nih.gov/41486154/)
7. [MuSA: a Nextflow pipeline for deep, reproducible annotation and clinical ranking of genomic variants.](https://pubmed.ncbi.nlm.nih.gov/42304195/)
8. [DataLad: distributed system for joint management of code, data, and their relationship.](https://pubmed.ncbi.nlm.nih.gov/39469147/)
9. [Curation of BIDS (CuBIDS): A workflow and software package for streamlining reproducible curation of large BIDS datasets.](https://pubmed.ncbi.nlm.nih.gov/36064140/)
10. [PoLoCo: a reproducible pooled low-coverage workflow for draft genome assembly and allele frequency analysis from ethanol-preserved small non-model invertebrates.](https://pubmed.ncbi.nlm.nih.gov/42924100/)
11. [Reusable tutorials for using cloud-based computing environments for the analysis of bacterial gene expression data from bulk RNA sequencing.](https://pubmed.ncbi.nlm.nih.gov/38997128/)