Dependency Resolution and Supply-Chain Security
How a packaging tool turns “I want these packages” into a consistent set — and how to keep that set from becoming an attack vector.
Dependency resolution — the problem
You declare django>=5.2 and some-lib. But some-lib depends on asgiref<3.7, and django depends on asgiref>=3.8. There is no version of everything that satisfies all constraints — a conflict. The resolver’s job is to find a consistent set, or prove none exists.
This is genuinely hard — it’s a constraint-satisfaction / SAT-like problem. Each package can have many versions, each version has its own dependency constraints, and they all have to agree.
“Dependency hell” and the diamond problem
your app ──► A ──► requests >= 2.30
└─► B ──► requests < 2.28
A and B both depend on requests, with incompatible constraints. The resolver can’t pick a requests version that satisfies both. This is the diamond dependency problem. Real resolutions:
- One of A or B has a newer release with a relaxed constraint → upgrade it.
- The constraint is overly strict → file an issue / pin around it.
- Genuinely incompatible → you can’t use both at those versions; something has to give.
A modern resolver (modern pip, uv, Poetry) detects this and fails loudly with an explanation rather than installing an inconsistent set. Old pip would sometimes just install whatever it grabbed last — silently broken.
Backtracking
When the resolver’s first choice leads to a conflict, it backtracks — tries an earlier version of some package and re-resolves. On a large graph this can be slow (it’s why Poetry’s resolver had a reputation for hanging, and why uv’s fast Rust resolver is a real improvement). The output of a successful resolution is the locked graph — see the lockfiles file.
Version constraints
| Operator | Meaning |
|---|---|
==1.2.3 |
exactly this version |
>=1.2,<2.0 |
a range — “1.x at or above 1.2” |
~=1.2.3 |
compatible release — >=1.2.3,<1.3.0 |
^1.2.3 (Poetry) |
caret — >=1.2.3,<2.0.0 (SemVer-aware) |
!=1.4.0 |
exclude a known-bad version |
| (none) | any version — avoid; one bad release breaks you |
For an application: declare reasonable ranges, let the lockfile pin the exact graph. For a library: declare loose ranges so consumers can resolve compatibly — never exact pins in a published library.
Supply-chain security — the dependency graph as an attack surface
Every dependency (and every transitive dependency) is code you run with your application’s privileges. The threats:
Known vulnerabilities (CVEs)
A version of a dependency has a publicly-disclosed security flaw. You may be running it without knowing.
- Scan:
pip-audit,safety,osv-scanner, GitHub Dependabot /dependabot.yml, Snyk. They cross-reference your lockfile against vulnerability databases. - Run it in CI — fail or warn the build on a known-vulnerable dependency.
- The fix is usually “bump to the patched version” — which is a deliberate lockfile update (see lockfiles file).
Typosquatting
A malicious package named close to a real one (requets, python-dateutil vs a fake) — install it by typo and you’ve run attacker code. Mitigations: care with package names, lockfiles (you don’t retype names on every install), and private-index allowlists.
Dependency confusion
You have an internal private package mycompany-utils. An attacker publishes a public package with the same name and a higher version number. A misconfigured installer prefers the public one. Mitigations: configure the installer to use only your private index for internal package names, or scope/namespace internal packages.
Compromised packages / malicious releases
A real, legitimate package’s maintainer account is compromised, or a maintainer goes rogue, and a malicious version is published. This is the hardest one. Mitigations:
- Hash pinning — the lockfile records a hash per artifact; install verifies the download matches. Stops a tampered artifact, but not a maliciously-published-then-hashed release.
- Pin versions, update deliberately — don’t auto-pull the latest; a new release is a reviewed change.
- Delay adoption — don’t install a release the day it drops; let the community catch obvious malware first.
- Minimize the dependency tree — fewer deps, smaller attack surface. Question whether you need a dependency for something trivial (the left-pad lesson).
Hash verification
# in a hashed requirements.txt / lockfile
requests==2.31.0 --hash=sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f
pip install --require-hashes (and uv/Poetry by default with their locks) verifies every downloaded artifact against the recorded hash. If a package index is compromised and serves a tampered file, the hash mismatch aborts the install. This is integrity protection — it doesn’t help if the legitimately published version is itself malicious.
SBOM — Software Bill of Materials
A machine-readable inventory of every dependency (and transitive dependency) in your application — formats like CycloneDX or SPDX. Generated from the lockfile (pip-audit and others can emit one). Increasingly required for compliance/regulated contexts; it lets you answer “are we affected by the new CVE in package X?” instantly instead of grepping repos.
Practical posture
A reasonable senior-level baseline:
- Lockfile committed, full graph pinned + hashed.
pip-audit(or equivalent) in CI — build flags known CVEs.- Dependabot / Renovate opening PRs for dependency updates — each is a reviewable lockfile-diff PR.
- Private internal packages resolved only from the private index (no dependency-confusion).
- Deliberate updates — no auto-merge of dependency bumps without CI passing; don’t adopt bleeding-edge releases instantly.
- Minimal tree — fewer dependencies is fewer risks; don’t pull a package for a five-line function.
Common gotchas
- Old pip silently installing an inconsistent set — modern resolvers fail loudly; if you’re on something ancient, that’s a real risk.
- Slow resolution / hangs — backtracking on a large graph; uv’s fast resolver is the fix.
- No vulnerability scanning — you’re running known-CVE dependencies and don’t know it.
pip-auditin CI. - Unpinned / unhashed installs — no integrity guarantee; a compromised index can serve tampered artifacts.
- Internal package names installable from public PyPI — dependency-confusion risk; lock internal names to the private index.
- Auto-merging dependency bumps — a malicious or breaking release lands without review. Updates are reviewed changes.
- Huge dependency trees for trivial needs — every transitive dep is attack surface and a potential conflict.
Interview angle
- “What does a dependency resolver do?” — turns your declared constraints into a single consistent set of exact versions where every package’s requirements (including transitive ones) are satisfied — or proves no such set exists and fails. It’s a constraint-satisfaction problem; the resolver backtracks when a choice leads to a conflict.
- “What’s the diamond dependency problem?” — two of your dependencies require incompatible versions of a common transitive dependency. The resolver can’t satisfy both; you upgrade one of the offenders to a release with a relaxed constraint, or you can’t use both at those versions. A modern resolver fails loudly here; old pip would silently install something broken.
- “How do you secure your dependency supply chain?” — committed + hashed lockfile (integrity),
pip-audit/Dependabot in CI (catch known CVEs), private-index-only resolution for internal package names (block dependency confusion), deliberate reviewed updates rather than auto-pulling latest, and a minimal dependency tree (less attack surface). - “What’s dependency confusion?” — you have an internal package
mycompany-utils; an attacker publishes a public package with the same name and a higher version; a misconfigured installer prefers the public one and runs attacker code. Fix: configure the installer to resolve internal names only from your private index. - “What does hash pinning protect against — and what doesn’t it?” — it verifies each downloaded artifact matches the lockfile’s recorded hash, so a compromised package index serving a tampered file is caught. It does not protect against a maliciously-published-then-hashed release — for that you need deliberate, delayed, reviewed adoption of new versions.
- “What’s an SBOM and why does it matter?” — Software Bill of Materials: a machine-readable inventory of every direct and transitive dependency, generated from the lockfile. When a new CVE drops, you query the SBOM to answer “are we affected?” instantly instead of auditing repos by hand. Increasingly a compliance requirement.