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.txtinstalls exactly the same versions - CI/CD builds match local dev builds
- Production deploys are predictable
What Makes an Install Reproducible?
- Pinned versions — exact versions, not ranges
- Hashes — cryptographic verification of package contents
- Python version pinned — e.g., Python 3.11.8, not "3.11"
- 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
| Tool | Lockfile | Hashes | Speed | Complexity |
|---|---|---|---|---|
pip freeze | requirements.txt | ❌ | Fast | Low |
pip-compile | requirements.txt | ✅ | Medium | Low |
uv | requirements.txt | ✅ | Very fast | Low |
poetry | poetry.lock | ✅ | Medium | Medium |
pdm | pdm.lock | ✅ | Fast | Medium |
| Docker | Image digest | ✅ | Slow build | High |
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