Skip to main content

Reproducible Installs

What You'll Learn

How to make Python environments fully reproducible — same versions, same behavior, everywhere.

The Reproducibility Problem

# Developer A on Monday
pip install requests # gets requests 2.31.0

# Developer B on Wednesday
pip install requests # gets requests 2.32.0 (new release)

# Same requirements.in, different behavior — subtle bugs

Reproducibility means:

  • Every pip install -r requirements.txt installs exactly the same versions
  • CI/CD builds match local dev builds
  • Production deploys are predictable

What Makes an Install Reproducible?

  1. Pinned versions — exact versions, not ranges
  2. Hashes — cryptographic verification of package contents
  3. Python version pinned — e.g., Python 3.11.8, not "3.11"
  4. OS-appropriate wheels — the right binary for each platform

pip freeze — The Simplest Approach

pip freeze > requirements.txt

Output (fully pinned):

certifi==2024.2.2
charset-normalizer==3.3.2
idna==3.6
requests==2.31.0
urllib3==2.2.1

Install later:

python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt

Limitation: captures all packages (direct + transitive) mixed together. Hard to know which you actually chose.

pip-compile — The Right Approach

pip-compile maintains separate source (*.in) and lock (*.txt) files:

pip install pip-tools

# requirements.in — YOUR choices
cat > requirements.in << EOF
requests>=2.28
flask>=3.0
python-dotenv
EOF

# Generate locked requirements.txt
pip-compile requirements.in
# → produces requirements.txt with all packages pinned + comments

# Install from lockfile
pip-sync requirements.txt # installs exactly, removes anything extra

Hash Verification

Add --generate-hashes for maximum security — pip verifies the file hasn't been tampered with:

pip-compile --generate-hashes requirements.in

Output:

requests==2.31.0 \
--hash=sha256:58cd2187423d... \
--hash=sha256:942c5a758f98...
# via -r requirements.in

Install with hash checking:

pip install --require-hashes -r requirements.txt

Now pip refuses to install a package if its hash doesn't match — protects against supply chain attacks.

Pinning Python Version

Use .python-version with pyenv:

# Install pyenv and a specific Python
pyenv install 3.11.8
pyenv local 3.11.8 # writes .python-version file

# Now everyone on the team uses 3.11.8
cat .python-version
# 3.11.8

In CI/CD, pin the Python version explicitly:

# GitHub Actions
- uses: actions/setup-python@v5
with:
python-version: "3.11.8" # exact version, not "3.11"

Docker — Fully Reproducible Environment

Docker captures the OS, Python version, and all packages together:

# Dockerfile
FROM python:3.11.8-slim

WORKDIR /app

# Install dependencies first (Docker cache layer)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Copy source code
COPY src/ ./src/

CMD ["python3", "-m", "myapp"]
docker build -t myapp:1.0.0 .
docker run myapp:1.0.0

The same Docker image runs identically on any machine or cloud provider.

CI/CD Reproducible Install Pattern

# .github/workflows/ci.yml
name: CI

on: [push, pull_request]

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: "3.11.8" # pinned

- name: Cache pip
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('requirements.txt') }}

- name: Install dependencies
run: |
pip install pip-tools
pip-sync requirements.txt requirements-dev.txt

- name: Run tests
run: pytest tests/ -v

Keeping Lockfiles Updated

# Weekly dependency update workflow
pip-compile --upgrade requirements.in
pip-compile --upgrade requirements-dev.in -o requirements-dev.txt
pip-sync requirements-dev.txt
pytest tests/ # verify nothing broke
git add requirements*.txt
git commit -m "chore: weekly dependency update $(date +%Y-%m-%d)"

Set up automated dependency PRs with Dependabot:

# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: "pip"
directory: "/"
schedule:
interval: "weekly"

Comparing Reproducibility Tools

ToolLockfileHashesSpeedComplexity
pip freezerequirements.txtFastLow
pip-compilerequirements.txtMediumLow
uvrequirements.txtVery fastLow
poetrypoetry.lockMediumMedium
pdmpdm.lockFastMedium
DockerImage digestSlow buildHigh

For most projects: pip-compile or uv is the sweet spot.

Quick Reference

# pip-compile workflow
pip install pip-tools
pip-compile requirements.in # generate lockfile
pip-compile --upgrade requirements.in # upgrade all
pip-compile --generate-hashes requirements.in # with hashes
pip-sync requirements.txt # install exact env

# Pin Python version
pyenv local 3.11.8
echo "3.11.8" > .python-version

# Docker
FROM python:3.11.8-slim
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Check install is clean
pip check # no broken dependencies
pip list # see everything installed

What's Next

Module 9: Automation and Scripting