Pin each requirement with ==, add a sha256 hash for every distribution, and run pip in hash-checking mode. Pip then refuses any file whose hash does not match your requirements file. The pip documentation says this protects against remote tampering and network problems, though not against a package that was malicious when you first hashed it.
What hash-checking mode does
By default, the pip documentation says, pip does not check for remote tampering. Hash-checking mode changes that. You embed a hash per requirement in requirements.txt and pip verifies each downloaded file against it.
FooProject==1.2 \
--hash=sha256:2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824 \
--hash=sha256:486ea46224d1bb4fb680f34f7c9ad96a8f24ec88be73ea8e5a6c65260e9cb8a7
Those hashes are the example values from pip's own documentation. You will generate your own.
The rules pip enforces
The documentation lists restrictions that surprise people.
- Hash-checking is all or nothing. If any requirement has a hash, hashes are required for all of them. A partly hashed file is of little use, because a bad actor could slip code in through the unhashed entry.
- Every dependency must be spelled out and hashed, including transitive ones. A dependency that is missing from the file causes an error.
- Requirements must be pinned to an exact version with
==, or to a URL or filesystem path. This avoids a surprise hash mismatch when a new release matches a loose specifier. - A newer pip release added an option to turn off the automatic enforcement for mixed cases, such as local directories or version control URLs. Check the documentation for your pip version before relying on it.
A practical workflow
- Write your direct dependencies with loose, human friendly ranges in a source file.
- Resolve them into a fully pinned list including transitive packages. Pip itself has an experimental lock command, and other tools exist for this job. Treat the output as generated, not hand edited.
- Add hashes for each pinned file. The pip hash command prints a hash for a local archive, and requiring hashes on an install shows the hashes of what it fetched, which is a convenient way to bootstrap the list.
- Install in CI and production with hash checking required.
- Regenerate on a schedule, review the diff, and let tests decide.
python -m pip install --require-hashes -r requirements.txt
Binary and source distributions
The same documentation suggests a second hardening step: refuse source distributions, so installation does not run arbitrary build code. The option is only-binary with the value all. Some packages publish no wheel for your platform, so test before enforcing it.
Because a package can have both a wheel and a source archive, you may need hashes for both. The documentation notes that you may need to add hashes for alternative archives with pip hash.
What hashes do not tell you
A hash proves the file is the one you chose. It says nothing about whether that file is safe. Pair pinning with vulnerability and malicious package checks.
Where Safeguard fits
Safeguard's dependency scanning and SBOM generation can show what a pinned Python tree contains and flag known vulnerabilities in it. A free scan is at gold.safeguard.sh/scan.
Quick check
Do I need hashes for transitive dependencies?
Yes. In hash-checking mode every dependency must be listed and hashed, or pip reports an error.
Does a hash make a package safe?
No. It proves the file matches the one you hashed earlier. If that file was bad at the time, the hash will happily match it.
Sources
Last reviewed: 19 October 2026.