What does pip's `--require-hashes` mode enforce when installing a requirements file?
answer
- Versions name a release, hashes name bytes
- The file must be closed, not just pinned
- Transitive entries need hashes too
- One hash per acceptable artefact
- Editable installs cannot participate
basics
~20 sIt makes pip reject any downloaded artefact whose hash is not listed in the requirements file. It also forces every requirement, transitive ones included, to be pinned with == and carry at least one hash, so nothing unlisted can install.
solid answer
~50 s`--require-hashes` turns on pip's hash-checking mode explicitly. In that mode every requirement must be pinned with `==` and annotated with one or more `--hash=sha256:...` values; pip downloads each artefact, hashes the bytes, and aborts the whole install unless a listed hash matches. The knock-on effect is stricter than the name suggests: because *every* requirement must be hashed, the file has to be **complete**, listing the full transitive closure -- so a dependency that would otherwise be pulled in silently causes a loud failure instead. Editable requirements are rejected outright, since there are no bytes to hash. Hash-checking mode also switches on implicitly the moment any single requirement carries a hash; the flag exists to fail loudly when someone adds an unhashed line. Multiple hashes per requirement are allowed, one per acceptable artefact -- typically the source distribution plus each platform wheel you install on.
code
python · 5 linesimport hashlib
artefact_bytes = b"pretend these are the wheel's bytes"
digest = hashlib.sha256(artefact_bytes).hexdigest()
print(f"frame-tools==2.3.1 --hash=sha256:{digest}")go deeper
Recall that a version pin names a release while a hash names the exact file, and that pip can refuse to install anything whose hash is not listed. Knowing the flag exists is enough at this level.
Explain the three conditions the mode imposes -- exact pins, hashes on every requirement including transitive ones, and no editable entries -- and why those together mean nothing unlisted can install.
Show where it belongs in a real pipeline: which environments enforce it, how hashes are generated for each deployment target, and what it genuinely defends against versus what it cannot touch, such as the local build of a source distribution.
Weigh the guarantee against its cost across many services: who regenerates hashed files, whether an internal index makes the mode redundant or complementary, and where the friction is worth accepting rather than imposed everywhere by default.
### The mechanism Each line in a hashed requirements file looks like a pin plus one or more hash annotations: ``` frame-tools==2.3.1 \ --hash=sha256:9f2c... # the linux wheel --hash=sha256:41ab... # the sdist ``` pip resolves the file, downloads the artefact it selected, computes its SHA-256 over the file bytes, and compares against the listed values. One match is enough. No match, and the install aborts -- not the single package, the whole run, before anything is placed on disk. `pip hash <file>` computes an annotation for a local artefact in exactly this format, which is useful when you vendor a wheel; in practice the hashes are generated for you by the same tool that compiles the pins. ### Why the mode is stricter than 'check the bytes' Hash-checking mode imposes three conditions that together are the real security property: 1. **Every requirement must be pinned with `==`.** A range would let pip pick a release whose hash could not have been known when the file was written. 2. **Every requirement must be hashed, including transitive ones.** pip cannot install a package that is not in the file, because an unlisted package has no hash. This is what makes the requirements file *closed*. 3. **Editable and unhashable requirements are rejected.** There is no fixed artefact behind `pip install -e .`, so it cannot participate. Condition 2 is the one people underestimate. It means a compromised or confused resolution cannot smuggle in an extra distribution: if a package you never listed appears in the graph, the install fails rather than proceeding. It closes the dependency-confusion shape where a name resolves against an unexpected index, because the substituted artefact's bytes will not match -- and a substituted *name* is not in the file at all. Note also that hash-checking mode turns on implicitly: as soon as **any** requirement in the file carries a `--hash`, pip requires hashes for all of them. Passing `--require-hashes` makes the intent explicit and produces a clear error if someone appends an unhashed line, rather than depending on the file already having one. ### What it protects against, and what it does not **It does protect against** an artefact being replaced after publication on an index or a mirror, a poisoned local wheel cache or internal proxy, a corrupted download, and an install pulling something from an unexpected extra index. **It does not protect against** a malicious release you deliberately pinned and hashed -- the hash proves the bytes are the ones you resolved, not that the code is good. It does not protect the *build*: a hashed source distribution is verified and then built locally, and a build step can execute arbitrary code and produce different output on different machines. And it does nothing after installation. ### The operational cost Hashes must be regenerated on every version bump, so the file is strictly a generated artefact -- hand-editing it is not viable. Coverage is per artefact, so a project deploying to more than one platform or interpreter version needs every wheel it might select to be hashed, or a separate compiled file per target. Teams that hit friction here usually mis-diagnose it as 'hashes are painful' when the real cause is compiling on the developer's laptop instead of in a job that targets the deployment environment. The pragmatic split most teams land on: the application's deployment path installs from a hashed, fully-pinned file; developer environments and library test matrices install from the lock or from ranges, where the friction would buy less. ### Where lockfiles fit A modern lockfile already stores per-artefact hashes for every entry and every platform it covers, so 'hash pinning' becomes a property of the lock rather than a separate discipline. `--require-hashes` remains the way to get the same guarantee when the deployment tooling only speaks requirements files -- which is why lock-producing tools offer an export to a hashed requirements file.
- Why does hash-checking mode force every transitive dependency into the requirements file?Because a package with no hash cannot be installed, and pip will not guess one. Requiring hashes therefore requires completeness: the file must name the entire resolved closure. That is the actual security property -- nothing outside the file can enter the environment -- and it is why the file must be produced by a resolver rather than written by hand.
- A requirement lists two `--hash` values for the same version. Is that a mistake?No. Hashes are per artefact, not per release, so a single pinned version legitimately has one hash for its source distribution and one for each platform wheel you might install. Listing several lets one file serve several targets; pip accepts the install when the artefact it actually selected matches any listed hash.
- Does hashing a source distribution make the resulting install reproducible?It makes the *input* verifiable, not the output. A source distribution is built locally, and the build runs code that can consult the environment, the compiler, or the network, so two machines can produce different wheels from identical verified bytes. If you need the built artefact to be identical everywhere, build it once in a controlled job, publish the wheel, and hash-pin that.
A version pin names the model of the part you ordered; a hash is the tamper-evident seal on the box. Only the second tells you nobody swapped the contents in transit.
saying these in an interview costs you the question
- Thinks pinning a version already verifies the artefact bytes
- Believes hashes make a malicious pinned release safe
- Assumes only direct dependencies need hash annotations
- Expects one hash per requirement to cover every platform
- Hand-edits hashes instead of regenerating the file
- Claims a hashed source distribution guarantees an identical build