⚙️ How to distribute your software to foster reproductibility¶
Warning
The aim of this card is to provide examples of tools allowing to package your own piece of software to allow others to easily execute it in a frozen (fixed) environment.
For further information on packaging, please have a look at package manager. For further information on containers, please have a look at containers for reproducibility. Package managers handle atomic packages. Containers allow to generate frozen environments.
The examples in this card cover three common approaches:
Each section includes a short explanation, benefits and limitations regarding reproducibility, a minimal build recipe, and practical recommendations.
1. Python Packaging (PyPI)¶
Packaging a Python project involves writing a recipe (e.g., pyproject.toml or setup.py) that defines metadata, dependencies, and build instructions. The build phase generates a distributable wheel or source archive using tools like build or setuptools. Once built, the package can be uploaded to PyPI (or a private index), making it accessible for others to install via pip (the standard Python package installer), conda, or uv (a modern, fast, pip-compatible installer with built-in dependency locking).
For detailed information on Python Packaging see https://packaging.python.org/en/latest/tutorials/packaging-projects/.
Benefits¶
- Native solution for Python ecosystems
- Easy distribution and installation via pip, conda, uv, etc
- Large ecosystem of pre-built packages
- Low overhead compared to containers
Limitations (Reproducibility)¶
- Dependencies may have transitive dependencies with version conflicts.
- Dependencies on C libraries, frequent in the Python ecosystem, are not managed at all.
- Virtual environments are required to avoid system-wide version conflicts.
pipalone does not provide deterministic resolution- No guarantee of identical behavior across operating systems.
Minimal build recipe example (pip)¶
pyproject.toml
[project]
name = "my-package"
version = "0.1.0"
dependencies = [
"numpy==1.24.0", # Pin versions
"requests==2.31.0",
]
[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"
Build & install¶
pip install build python -m build
Distribute¶
Once built, the package can be uploaded to PyPI (or a private index), making it accessible for others to install via pip (the standard Python package installer), conda or uv (a modern, fast, pip-compatible installer with built-in dependency locking).
Reproducibility tips¶
- Always pin exact versions (e.g., numpy==1.24.0).
- Use a virtual environment (python -m venv venv).
- Document/record the Python version used (e.g., python==3.11.6).
- Consider tools like uv for dependency management.
2. Containers (Docker)¶
Packaging with Docker requires writing a Dockerfile, which serves as a recipe for the image, specifying the base environment, dependencies, and commands. The build phase creates a self-contained image with all dependencies frozen. The resulting image can be pushed to a registry (e.g., Docker Hub, GitHub Container Registry), allowing others to pull and run it anywhere Docker is available. The image built from a given Dockerfile represents a frozen environment as explained in "Evolving, frozen, and reproducible environments"
Benefits¶
- Strong isolation from host system
- Includes system libraries and runtime
- Easy to run anywhere Docker is available
Limitations (Reproducibility)¶
- Base images can change over time
- Docker builds are not fully deterministic by default
- Requires container runtime support (note that Docker images can also run with apptainer)
Dockerfile example¶
FROM python:3.10-slim # fixed version of the base image
RUN apt-get install -y --no-install-recommends git
RUN git clone https://github.com/docker/getting-started-app.git #git clone allows to render the source code available in the image for the future users, fostering thus transparency
Build & run¶
docker build -t mytool:0.1.0 . docker run --rm mytool:0.1.0
Distribute¶
The resulting image can be pushed to a registry (e.g., Docker Hub, GitHub Container Registry), allowing others to pull and run it anywhere Docker is available.
Reproducibility tips¶
- Pin base images and dependency versions
- Avoid "latest" tag (e.g., FROM ubuntu:latest)
- Avoid non-deterministic build steps (timestamps, random seeds)
- See this explanation about constructing reproducible container images.
3. Functional Packaging (Guix)¶
Packaging with Guix involves writing a Scheme file that declaratively defines the package, its dependencies, and build steps. The build phase uses Guix to create a reproducible, isolated package, much like Nix, which relies on the Nix Expression Language to achieve similar goals. Both systems ensure that the build environment is pure and deterministic, but Guix distinguishes itself by using Scheme, a Lisp dialect, for its definitions. The result can be shared via a Guix channel or a substitute server, enabling others to install it in an identical environment using guix package or guix environment.
Benefits¶
- Very strong reproducibility guarantees
- Full dependency graph is captured
- Multiple versions can coexist safely
Limitations¶
- Steeper learning curve
- Smaller ecosystem than PyPI/Docker
- Requires Guix installation
Minimal build recipe¶
mytool.scm
(use-modules (guix packages)
(guix git-download)
(guix build-system python)
(gnu packages python))
(package
(name "mytool")
(version "0.1.0")
(source (local-file "." #:recursive? #t))
(build-system python-build-system)
(propagated-inputs
(list python-numpy python-click))
(home-page "https://example.org")
(synopsis "My reproducible tool")
(description "Example Guix-packaged Python tool.")
(license #f))
Build & run¶
guix build -f mytool.scm
guix shell -f mytool.scm -- mytool
Distribute¶
The result can be shared via a Guix channel or a substitute server, enabling others to install it in an identical environment using guix package or guix environment.
Reproducibility tips¶
- Use fixed Guix channels (channels.scm)
- Use
guix time-machinefor long-term builds - Run your research computations in a containerized shell:
guix shell -C - See this explanation for more details on making computations with Guix fully reproducible