How to Pin Transitive Dependencies in requirements.txt to Pass Security Audits
A snake shedding its skin doesn’t merely slough off the old layer; it carefully ensures the new integument forms a seamless, protective barrier. Any gap or loose scale invites parasites and infection. In Python development, requirements.txt acts as that protective layer. When we fail to pin transitive dependencies, we leave gaps in our security armor—vulnerabilities in indirect packages that slip through during installation, often undetected until a security audit fails.
Security scanners like pip-audit, Safety CLI, and Snyk flag unpinned transitive dependencies as high-risk—vulnerable indirect packages slip in during installs. Pinning all deps (direct + transitive) with exact versions/hashes in requirements.txt ensures reproducible, auditable builds. This guide covers pip-tools (gold standard) and uv (fast alternative) for Python 3.12+ projects.
Why Pin Transitive Dependencies?
To understand why pinning transitive dependencies is critical, consider the anatomy of a Python application. When you install a package like Flask, you aren’t merely installing Flask; you’re installing a hierarchy of dependencies—Werkzeug, Jinja2, and their own dependencies. These are transitive dependencies.
When these dependencies are unpinned (loose constraints like flask>=2.3.0), the resolver selects the latest compatible versions at install time. This introduces three major risks:
- Version Drift: A new release of a transitive dependency might introduce breaking changes or performance regressions. One day
pip install flaskpulls version 2.4.0 of Werkzeug, and your application crashes because it relied on a behavior specific to 3.0.x. - Security Gaps: Vulnerabilities are often discovered in deep dependency trees. If you don’t pin the exact version,
pipmight resolve to a version containing a known CVE (Common Vulnerabilities and Exposures) without you realizing it. Security scanners flag this as “unpinned dependencies” because they cannot verify the integrity of the chain. - Compliance Failures: Standards like SOC2 and PCI-DSS require reproducible builds and locked dependencies to ensure that what you tested in staging is exactly what runs in production. Unpinned transitive dependencies make this impossible.
By pinning all dependencies—direct and transitive—with exact versions and hashes, requirements.txt becomes a true lockfile. It guarantees that every install, on every machine, pulls the exact same code, creating a reproducible, auditable, and secure foundation.
Prerequisites
Before we begin pinning dependencies, we need a specific project structure. Python 3.12+ is recommended because it includes modern dependency resolution features, though these tools work with older versions too.
You’ll need one of the following tools installed:
- pip-tools: The established standard for dependency management, generating a fully locked
requirements.txtfrom a simple input file. - uv: A fast, Rust-based alternative to pip-tools that produces identical output but with significantly better performance for large projects.
The core concept here is the separation of concerns: we distinguish between direct dependencies (what your project explicitly needs) and transitive dependencies (what your direct dependencies need).
Create a file named requirements.in in your project root. This file lists only your direct dependencies with loose version constraints:
Example requirements.in:
flask>=2.3.0
requests
pytest
This file represents your intent—what you want to install. The tool will resolve the complete dependency tree, including all transitive dependencies, and generate a locked requirements.txt file that represents the exact state of your environment.
Method 1: pip-compile (Recommended)
pip-tools is the established standard for dependency management in Python. It works by reading your requirements.in file, resolving all direct and transitive dependencies, and generating a locked requirements.txt file with exact versions.
Installation: First, install pip-tools into your global Python environment or a dedicated virtual environment:
$ pip install pip-tools
Basic Compilation:
The simplest usage reads requirements.in and outputs requirements.txt:
$ pip-compile requirements.in
This command resolves your dependencies and pins them to specific versions. The output file includes comments explaining where each dependency came from.
Secure Compilation with Hashes:
For security audits, you need cryptographic hashes to verify package integrity. Use the --generate-hashes flag:
$ pip-compile requirements.in --generate-hashes --output-file=requirements.txt
This generates a requirements.txt file where every package includes SHA-256 hashes. Security scanners like pip-audit can verify these hashes match the actual package contents, ensuring no tampering has occurred.
Development Dependencies:
If you manage dependencies in pyproject.toml with optional groups (like dev), pip-tools can compile those too:
$ pip-compile pyproject.toml --extra dev --generate-hashes
This creates a separate locked file for development dependencies while keeping production dependencies isolated.
Output requirements.txt:
##
# This file is *NOT* the original file, and is not intended for upwards compatibility.
# Run `pip-compile requirements.in` to update it.
flask==3.0.3 \
--hash=sha256:... \
# ... transitive like Werkzeug==3.0.4 --hash=...
Verify:
$ python -m venv test-env
$ source test-env/bin/activate
$ pip install -r requirements.txt --dry-run # Check resolution
$ deactivate; rm -rf test-env
Method 2: uv pip-compile (10x Faster)
If pip-tools feels slow for your project, uv offers a compelling alternative. Written in Rust, uv provides the same functionality as pip-tools but with significantly improved performance, especially for large monorepos or complex dependency trees.
Installation: uv can be installed via the official installer script:
$ curl -LsSf https://astral.sh/uv/install.sh | sh
Basic Compilation: The command syntax is slightly different but conceptually identical to pip-tools:
$ uv pip compile requirements.in -o requirements.txt --hashes
The -o flag specifies the output file, and --hashes includes cryptographic checksums for security scanning.
Development Dependencies:
Like pip-tools, uv supports compiling from pyproject.toml with extras:
$ uv pip compile pyproject.toml --extra dev --hashes -o requirements-dev.txt
Performance Benefits: uv’s Rust implementation provides 10x faster dependency resolution compared to pip-tools. This is particularly noticeable in monorepos with many packages or when frequently updating dependencies. The output format is identical to pip-tools, making it a drop-in replacement.
Audit Your Locked requirements.txt
$ pip-audit -r requirements.txt
$ pipx install safety
$ safety check -r requirements.txt
$ snyk test --file=requirements.txt
All pass with pinned/hashed deps.
CI/CD Integration
Pre-generate/commit requirements.txt.
GitHub Actions:
- name: Generate requirements.txt
run: uv pip compile requirements.in -o requirements.txt --hashes
- run: pip install -r requirements.txt
- run: pip-audit -r requirements.txt
Pre-commit Hook:
# .pre-commit-config.yaml
repos:
- repo: https://github.com/psf/requests
rev: v2.32.3
hooks:
- id: pip-compile
Troubleshooting
| Issue | Fix |
|---|---|
| Hash mismatches | Regenerate: pip-compile --generate-hashes |
| Platform wheels fail | Add --platform linux_x86_64 or export on CI target |
| Transitive unpinned | Ensure --generate-hashes; check pip show -r requirements.txt |
| Large files | Split prod/dev; use --upgrade-package flask selectively |
| Poetry? | poetry export -f requirements.txt --with-hashes |
Alternatives
- Poetry/uv project mode: Avoid requirements.txt entirely (
poetry install/uv sync). - pip freeze: Quick/dirty:
pip freeze > requirements.txt(non-reproducible).
Conclusion
Pinning transitive deps with pip-compile/uv turns requirements.txt into audit-proof lockfiles. Automate via CI/pre-commit. Upgrade to pyproject.toml next (11. Migrate Guide).
Related:
Questions? Comment your audit tool/stack.
Sponsored by Durable Programming
Need help maintaining or upgrading your Python application? Durable Programming specializes in keeping Python apps secure, performant, and up-to-date.
Hire Durable Programming