Dependency & Supply-Chain Security
Every third-party package you install runs with the full trust of your program — supply-chain security is the discipline of knowing exactly what you're trusting, and pinning it down.
A modern Python project rarely stands alone — it depends, directly and transitively, on dozens or hundreds of third-party packages pulled from PyPI (the Python Package Index) or private indexes. Every one of those packages runs arbitrary code with the same privileges as your own program, at install time (via setup.py/build scripts) and at import/runtime. Supply-chain security is the set of practices for managing that trust deliberately — knowing what you depend on, pinning exactly what you tested, and catching known-vulnerable or malicious packages before they run.
What Is Software Supply-Chain Risk
The "supply chain" is everything upstream of your own code that ends up running inside your program: direct dependencies you chose, their transitive dependencies you didn't explicitly choose, the build tooling that assembles them, and the package index that hosts them. A supply-chain attack compromises one of those upstream links instead of attacking your code directly — poisoning a popular package with malicious code, publishing a lookalike package under a similar name, or hijacking a maintainer's account to push a malicious release to a package millions of projects already trust.
This risk is real and has happened repeatedly across language ecosystems (Python included): malicious packages have been found on PyPI mimicking popular library names, and compromised releases of legitimate packages have shipped credential-stealing code. The defenses below don't eliminate the risk, but they shrink the attack surface and the blast radius considerably.
Dependency Auditing
Before and after adding a dependency, it's worth asking basic questions: is it actively maintained, does it have a reasonable number of independent users/downloads, does its source match what's published (for smaller or unfamiliar packages), and — critically — do you actually need it, or does the standard library already cover the need? Every dependency you add is code you're trusting to run, code you now must keep updated, and one more thing an attacker could compromise instead of attacking you directly. Minimizing dependency count is itself a security practice, not just a tidiness preference.
Pinned Versions
A dependency specifier can be loose ("any 2.x version") or exact ("exactly this version"). Loose ranges (requests>=2.0) are convenient for libraries (so they don't conflict with whatever version a consuming application already uses) but risky for applications, because the *exact* set of packages actually installed can silently change between two installs, weeks apart, as new versions are published — you could deploy code today that installs a different, untested dependency version tomorrow.
The common convention: libraries declare loose, compatible ranges so they play well with whatever else is installed; applications pin exact versions for reproducible, tested deployments.
# loose (common in a library's own dependency declaration)
requests>=2.28,<3
# pinned exact (common in an application's lock file / deployed environment)
requests==2.32.3
urllib3==2.2.3
certifi==2024.8.30Lock Files
A lock file records the exact, fully-resolved set of package versions (including every transitive dependency) that was tested and is meant to be installed, as opposed to a requirements file that may only list top-level, loosely-versioned dependencies. Several third-party tools generate and consume lock files: pip-tools' pip-compile produces a pinned requirements.txt from a looser requirements.in; Poetry and uv (both third-party project/dependency managers) maintain their own lock file formats (poetry.lock, uv.lock) automatically as part of normal dependency management. The standard library has no built-in lock-file mechanism — this is entirely ecosystem tooling.
# requirements.in (loose, human-edited)
requests>=2.28
click>=8.0
$ pip-compile requirements.in
# -> writes requirements.txt with every package (direct + transitive)
# pinned to an exact, tested version
$ pip install -r requirements.txt # reproducible install from the lock fileHashes
Pinning a version number alone still trusts the package index to serve the exact bytes you expect — a compromised index or a mirror could theoretically serve different content under the same version number. Hash-pinning records a cryptographic hash of the exact package file alongside the version, and pip install --require-hashes refuses to install anything that doesn't match, closing that gap. pip-compile --generate-hashes and Poetry/uv's lock files both include hashes by default as part of their locking process.
# requirements.txt with hashes (excerpt)
requests==2.32.3 \
--hash=sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d61d
urllib3==2.2.3 \
--hash=sha256:1f6be0d5d0c1e0b0ed9d3f2b4a8b1a2e6...
$ pip install --require-hashes -r requirements.txtTrusted Package Sources
PyPI is the default, official index most pip install commands pull from, and it does have some baseline protections (e.g. reserved/verified project names for well-known packages, malware scanning on upload), but it's an open platform anyone can publish to, so the burden of verification still falls partly on the installer. Typosquatting — publishing a malicious package under a name deliberately close to a popular one (reqeusts instead of requests, python-dateutil lookalikes, etc.) — is a real, recurring attack, relying entirely on a developer mistyping an install command or a dependency spec.
Private/internal package indexes (for organization-internal packages) need the same scrutiny — and configuring pip to prefer a private index correctly (so a malicious public package can't 'shadow' an internal one of the same name, a known class of attack called dependency confusion) is itself a security-relevant configuration choice.
# a typo that installs a different, unverified package entirely
$ pip install reqeusts # NOT the real "requests" package!
# double-check package names carefully, especially for less-common
# dependencies, and prefer copy-pasting from a project's own docs
# rather than typing from memoryVulnerable Dependency Scanning
Even a legitimately published, correctly named package can have known security vulnerabilities discovered after release. Third-party scanning tools check your installed/locked dependencies against vulnerability databases and flag matches: pip-audit (backed by the Python Packaging Authority's advisory database and OSV), and commercial/hosted options like GitHub's Dependabot alerts or Snyk. Running this in CI (see the CI/CD for Python topic) turns "a CVE was published for a package you depend on" into an actionable, automatic alert instead of something you'd only discover by accident.
$ pip install pip-audit
$ pip-audit
Found 1 known vulnerability in 1 package
Name Version ID Fix Versions
------- ------- ------------- ------------
requests 2.6.0 GHSA-xxxx-yyyy 2.20.0Software Supply-Chain Risks Beyond Direct Dependencies
Supply-chain risk extends past the packages you directly pip install: build tooling itself (a malicious setup.py executes arbitrary code at install time — one reason the modern pyproject.toml-based build backends and wheel-first installs reduce, though don't eliminate, this exposure), CI/CD pipeline dependencies and third-party GitHub Actions (a compromised action can exfiltrate secrets from every repo using it), and transitive dependencies you never explicitly chose but that got pulled in by something you did choose. A full picture of supply-chain risk considers the whole path from "code someone else wrote" to "code running in your production environment."
Practical Mitigations Summary
Putting the pieces together: use a lock file so installs are reproducible; pin (and ideally hash-verify) exact versions for applications; scan for known vulnerabilities regularly and in CI; minimize dependency count and audit new additions; double-check package names to avoid typosquatting; and isolate installs per project with virtual environments (see the Virtual Environments topic) so a compromised or misbehaving dependency in one project can't taint another.
Complete Example
A minimal but real supply-chain-conscious workflow: loose declaration for human editing, a locked+hashed file for reproducible/verified installs, and a scan step to catch newly disclosed CVEs.
# 1. Declare loose, direct dependencies
# requirements.in
requests>=2.28
click>=8.0
# 2. Lock to exact, hashed, tested versions
$ pip-compile --generate-hashes requirements.in
# -> requirements.txt (direct + transitive, pinned + hashed)
# 3. Install only what's in the lock file, verified by hash
$ pip install --require-hashes -r requirements.txt
# 4. Scan for known vulnerabilities, ideally in CI
$ pip-audit -r requirements.txtQuick Reference
Practice Tool/mechanism (mostly third-party)
--------------------- -----------------------------------------
Lock exact versions pip-tools (pip-compile), Poetry, uv
Verify file integrity --require-hashes, lock files with hashes
Scan for known CVEs pip-audit, Dependabot, Snyk
Avoid typosquatting careful copy/paste of exact package names
Avoid dependency correctly configured private index priority
confusion
Minimize surface fewer dependencies, prefer stdlib when it covers the need
Isolate installs virtual environments, per projectKey Takeaways
Every installed package runs with your program's full trust, at install time and at runtime — dependency count is itself a security surface.
Loose version ranges suit libraries; applications should pin exact, tested versions via a lock file for reproducible installs.
Hashes verify that the exact bytes installed match what was tested, closing the gap that version pinning alone leaves open.
Typosquatting and dependency confusion exploit small naming mistakes or misconfigured index priority — double-check package names and index configuration.
Scan dependencies for known vulnerabilities (pip-audit or similar) regularly and in CI, since a package can become vulnerable after you've already adopted it.
Unpinned dependencies in a deployed application mean the exact set of code running in production can silently change between deploys as new versions publish, without any of it being tested first.
$ pip install reqeusts # typo $ pip install requests # verified against the project's official docs
A single-character typo can install an entirely different, potentially malicious package under a similar name -- always verify package names, especially for less-familiar dependencies.
A dependency that was safe when pinned can have a vulnerability disclosed later; pinning alone doesn't protect against CVEs discovered after the fact -- ongoing scanning does.
Why is a loose version range like requests>=2.0 appropriate for a library but risky for a deployed application?
A library benefits from flexibility so it doesn't conflict with whatever version the consuming application already has; a deployed application needs reproducibility -- an untested new version resolving in on a fresh install could introduce silent behavior changes or bugs that were never tested.
What does a lock file add beyond a pinned requirements.txt?
A lock file records the full, exact, resolved dependency graph -- including every transitive dependency -- not just top-level packages, so an install is fully reproducible; tools like pip-compile, Poetry, and uv generate these automatically.
What security gap does hash-pinning close that version-pinning alone leaves open?
Version pinning still trusts the index to serve the exact bytes expected for that version; hash-pinning verifies the downloaded file's cryptographic hash matches what was recorded, so a compromised index or mirror serving different content under the same version number is caught.
What is typosquatting, in the context of Python packages?
Publishing a malicious package under a name deliberately similar to a popular one (e.g. a misspelling), relying on developers mistyping an install command or dependency spec to install the malicious package instead of the intended one.
What does a tool like pip-audit check for that version pinning by itself does not protect against?
Known vulnerabilities disclosed after a dependency was already pinned/adopted -- pip-audit checks installed/locked packages against vulnerability databases, catching CVEs published for a version you're already using.
Why does minimizing the number of dependencies count as a security practice, not just a code-cleanliness preference?
Every dependency is code that runs with your program's trust and is a potential compromise point (directly or via its own transitive dependencies); fewer dependencies means a smaller attack surface and less ongoing auditing/patching burden.
Are pip-audit, Poetry, and uv part of the Python standard library?
No -- they're all third-party tools you install and configure separately; pip itself (the installer) is bundled with Python, but lock-file management and vulnerability scanning are ecosystem tooling built on top of it.
Given a loose requirements.in with flask>=3.0, write the pip-tools command that produces a fully pinned, hash-verified requirements.txt.
pip-compile --generate-hashes requirements.in pip-compile resolves the full dependency graph (flask plus every transitive dependency) to exact versions and, with --generate-hashes, records a cryptographic hash for each, producing a requirements.txt suitable for --require-hashes installs.
Write the pip command that installs from a hash-pinned requirements.txt and refuses to install anything whose downloaded bytes don't match the recorded hash.
pip install --require-hashes -r requirements.txt --require-hashes enforces that every package listed has a hash and that pip verifies the downloaded file against it, failing the install rather than silently accepting mismatched content.
Write a short CI step (as a shell command) that fails the build if any locked dependency has a known vulnerability.
pip install pip-audit pip-audit -r requirements.txt pip-audit exits with a non-zero status when it finds known vulnerabilities in the scanned requirements file, which in a CI job causes that step (and the pipeline) to fail -- turning a newly disclosed CVE into an automatic, visible gate rather than something discovered by accident.