Pin dependency versions

Pin dependency versions with pip-tools — Secure development. Learn hands-on steps, troubleshooting, and what to study next.

Focus: pin dependency versions with pip-tools

Sponsored

You've written the code, tested it locally, and pushed it to production. Then one Tuesday, a transitive dependency releases a patch that breaks your app in a subtle way — or worse, a compromised version of a package you depend on silently lands in your environment. Without pinned versions, your production build is a moving target, and that's a security and stability risk you can't afford. This lesson shows you how to pin dependency versions with pip-tools, the industry-standard workflow for locking down every single package in your Python project.

The problem this lesson solves

When you install Python packages with pip install, it resolves the latest versions that satisfy your requirements. A file like requirements.txt with Django or requests means "any version," so today's deployment and next week's could pull entirely different code. This leads to two major problems:

  • Security vulnerabilities: You don't know which exact version is running, so you can't audit it against CVEs or reproduce a vulnerability report.
  • Non-reproducible builds: A teammate or CI environment installs different versions, causing "works on my machine" bugs and hard-to-trace production failures.

The fix is simple in concept: specify exact versions for every installed package — including the dependencies of your dependencies. But managing that manually is tedious and error-prone. That's where pip-tools steps in, automating the entire process.

Core concept / mental model

Think of your Python project as a tree. At the top are the packages you directly depend on, like flask or psycopg2. Below them are their dependencies, then their dependencies' dependencies, and so on — the transitive dependencies. Pinning only the top level leaves the rest of the tree vulnerable to change. To lock down the entire tree, you need a precise list of every leaf and branch.

  • requirements.in: Your declared, high-level dependencies (e.g., flask, psycopg2). This file is human-written and gives pip-tools its starting point.
  • requirements.txt: The generated, fully pinned output. Every package is listed with an exact version, and it's the file you install in production.
  • pip-tools: A tool that reads requirements.in, resolves the dependency graph, and generates requirements.txt. It's like a package-lock.json for Python, but with a more transparent, manual control flow.

In short, pip-tools separates "what I want" from "exactly what I get," and it ensures the latter is stable and auditable.

How it works step by step

  1. Create a virtual environment for your project to keep dependencies isolated.
  2. Install pip-tools into that environment: bash pip install pip-tools
  3. Write requirements.in listing your direct dependencies. Use version constraints like >= or ~= to allow some flexibility, but avoid bare names unless you want the latest at compile time.
  4. Run pip-compile to generate requirements.txt. This command reads requirements.in, resolves all transitive dependencies based on your Python version and platform, and writes exact versions.
  5. Install from the pinned file in your runtime environment: pip install -r requirements.txt. This is the command you use in Dockerfiles, CI pipelines, and production servers.
  6. Update dependencies deliberately when you want to pull in new versions — never just run pip install and hope.

A note on pip-compile output

By default, pip-compile writes to requirements.txt. The generated file includes a header comment explaining how it was produced, plus each package with a == version pin. It also includes hashes if you pass --generate-hashes, which we'll cover later.

Hands-on walkthrough

Let's put this into practice with a small Flask project.

Step 1: Set up and install pip-tools

mkdir mysecureapp && cd mysecureapp
python -m venv venv
source venv/bin/activate  # Windows: venv\Scripts\activate
pip install pip-tools

Step 2: Create requirements.in

# mysecureapp/requirements.in
flask
requests
psycopg2-binary

These are your direct dependencies. Notice: no version numbers. You're telling pip-tools, "give me something good," and it will decide the exact versions.

Step 3: Compile the lock file

pip-compile requirements.in

This creates requirements.txt. Here's a snippet of what it might contain:

#
# This file is autogenerated by pip-compile with Python 3.11
# from the following requirements:
#
#    requirements.in
#
blinker==1.7.0
    # via flask
click==8.1.7
    # via flask
flask==3.0.2
    # via -r requirements.in
markupsafe==2.1.5
    # via jinja2
psycopg2-binary==2.9.9
    # via -r requirements.in
requests==2.31.0
    # via -r requirements.in
urllib3==2.2.1
    # via requests
werkzeug==3.0.1
    # via flask

Notice how requests pulls in urllib3, and Flask brings Werkzeug, click, and more. These are transitive dependencies, and now they're locked.

Step 4: Install the pinned versions

pip install -r requirements.txt

Now your environment matches the lock file exactly. To confirm, run pip freeze — you'll see the same versions.

Step 5: Update a dependency safely

When you need to upgrade, say Flask, you update the version in requirements.in and re-run pip-compile:

# requirements.in
flask==3.1.0
pip-compile requirements.in
pip install -r requirements.txt

Now requirements.txt reflects an exact upgrade, and you can test it before deploying.

Pro tip: Always run pip-compile in the same Python version your production uses. The resolution can differ between Python releases — a package might require a newer interpreter.

Compare options / when to choose what

pip-tools isn't the only way to manage Python dependencies. Here's a quick guide:

Tool What it does Best for Trade-off
pip-tools Compiles requirements.in to a pinned requirements.txt Teams wanting explicit, reviewable lock files Requires an extra tool and manual compilation step
Poetry Full dependency manager with its own lock file New projects needing packaging and publishing Heavier lock file format, steeper learning curve
pip freeze Dumps current environment's exact versions Quick, one-off environment snapshots No resolution logic; freezes everything, including dev-only packages
Pipenv Similar to Poetry but simpler Simple apps with both production and dev dependencies Slower resolution, less active maintenance

When to choose pip-tools: If you already use requirements.txt and want minimal friction with a transparent, diff-friendly lock file. It works well with Docker and CI, and it integrates cleanly with pip-sync (which we'll see in a moment).

When to choose something else: If you're starting a new project and want a more modern, all-in-one solution, Poetry might be a better fit. If you need deterministic, repeatable installs across all environments, pip-tools is your friend.

Troubleshooting & edge cases

pip-compile takes too long

The resolver can be slow for large dependency graphs. Use the --verbose flag to see what's happening. You can also cache the resolution with a constraints.txt file that pins known-good versions, but that defeats the purpose — better to let it resolve.

"No matching distribution found"

If you see an error like ERROR: No matching distribution found for somepkg==1.0, the pinned version isn't available for your Python version or platform. Check if the package has a wheel for your environment, and consider using a version range instead of an exact pin.

Inconsistent lock files between platforms

pip-compile resolves based on your OS and Python version. A lock generated on Windows may differ from Linux for packages with architecture-specific code (e.g., psycopg2). Solution: generate the lock on the same platform as production, typically Linux in a Docker container.

Accidentally pinned a vulnerable version

Pinning is about reproducibility, not security guarantees. When a CVE is announced for a package you've pinned, run pip-compile --upgrade-package insecure-pkg to update just that one and commit the diff.

Managing multiple environments (dev vs prod)

Use separate input files:

# requirements-dev.in
-r requirements.in
pytest
black

Then compile both:

pip-compile requirements-dev.in

This generates requirements-dev.txt that includes both production and development dependencies.

Using hashes for supply-chain security

pip-tools can add SHA-256 hashes to your lock file with the --generate-hashes flag:

pip-compile --generate-hashes requirements.in

Now pip install will verify the integrity of each package against known hashes, protecting against tampered wheels. This is a strong addition to your secure development toolkit.

What you learned & what's next

You now understand the problem of non-reproducible dependencies and the mental model of separating direct vs. transitive dependencies. You can apply pip-compile to generate a fully pinned requirements.txt, update it safely, and use advanced features like hashes. You've also seen how to troubleshoot common resolution issues and maintain dev/prod splits.

Key takeaways:

  • Pip-tools turns a vague requirements.txt into a precise, auditable lock file.
  • Use requirements.in for your declared dependencies and requirements.txt for the compiled, pinned output.
  • Compile on the same platform and Python version as production for consistency.
  • Use --generate-hashes to guard against supply-chain attacks.
  • Promote any change to your lock file through code review, just like your source code.

Next step: In the next lesson, you'll learn how to use pip-audit to scan your pinned dependencies for known vulnerabilities — turning a reproducible environment into a verifiably secure one. You'll see how pip-tools and pip-audit complement each other in a hardened CI pipeline.

Pro tip: Combine pip-compile --generate-hashes and pip audit in your CI to catch both integrity and known vulnerabilities before they reach production.

Practice recap

Create a tiny Flask project, then run pip-compile to generate a requirements.txt. Study the transitive dependencies that appear, install them, and then upgrade one package using the --upgrade-package flag. If you're confident, add hashes with --generate-hashes and try pip-sync to see how it installs the exact locked versions.

Common mistakes

  • Pinning only top-level dependencies and ignoring transitive ones — leaves the entire subtree vulnerable to change.
  • Generating the lock file on a developer machine and deploying on a different platform, causing resolution conflicts.
  • Running pip install directly on a project without a lock file, bypassing the pinning process entirely.
  • Using pip freeze output as requirements.txt — this includes packaging tools like pip or setuptools you never intended to lock.

Variations

  1. Poetry is a full dependency manager with a built-in lock file and can replace pip-tools if you're starting fresh.
  2. Use pipenv for a simpler alternative with a Pipefile.lock and automatic environment management.
  3. Hand-write exact version pins in requirements.txt if you're working on a tiny, single-package project and don't need transitive resolution.

Real-world use cases

  • A web application uses pip-tools to lock flask and its dependencies, ensuring every server in the cluster runs identical code.
  • A data science project pins pandas and numpy to guarantee model reproducibility across teammates and CI machines.
  • A security-conscious organisation generates hashed lock files to protect against compromised packages in a multi-region deployment.

Key takeaways

  • Pinning dependency versions prevents non-reproducible builds and known supply-chain risks.
  • pip-tools compiles requirements.in into a fully pinned requirements.txt including transitive dependencies.
  • Always compile lock files on the same platform and Python version as production.
  • Use --generate-hashes to add integrity verification to every package.
  • Update dependencies deliberately by editing requirements.in and re-compiling, never by manual pip install.
  • Use separate input files to manage development and production dependency sets.

Sponsored

Sponsored

Discussion

Questions, corrections, and tips help everyone reading this page.

0 comments

Add a comment

Shown publicly with your comment.

Be constructive · max 4,000 characters

No comments yet — start the thread.

Related tutorials, quizzes, and articles for this topic.