backend / python core / packaging / 04_dependency_resolution_and_supply_chain.md

Dependency Resolution and Supply-Chain Security

6 interview angles 6 min read source

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:

  1. Lockfile committed, full graph pinned + hashed.
  2. pip-audit (or equivalent) in CI — build flags known CVEs.
  3. Dependabot / Renovate opening PRs for dependency updates — each is a reviewable lockfile-diff PR.
  4. Private internal packages resolved only from the private index (no dependency-confusion).
  5. Deliberate updates — no auto-merge of dependency bumps without CI passing; don’t adopt bleeding-edge releases instantly.
  6. 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-audit in 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.