pypa/pip · error · HashUnpinned
In --require-hashes mode, all requirements must have their…
Error message
In --require-hashes mode, all requirements must have their versions pinned with ==. These do not:
What it means
Raised by _get_linked_req_hashes (prepare.py:483) as HashUnpinned. In --require-hashes mode every (non-direct, non-file) requirement must be pinned with an exact '==' version so a hash can be meaningfully attached. If a requirement is not pinned, an unpinned version could later resolve to a different file and produce a confusing hash mismatch, so pip fails fast instead.
Solutions
- Generate a fully pinned+hashed requirements file with 'pip-compile --generate-hashes' (pip-tools).
- Pin each unpinned requirement to an exact version with == and add its sha256 hash.
- Run 'pip install --require-hashes <pkg>==<ver>' to let pip print the expected hash, then add it to the file.
Example fix
# before (requirements.txt) requests flask>=1.0 # after pip-compile --generate-hashes requirements.in -o requirements.txt # yields: requests==2.31.0 --hash=sha256:... flask==2.3.2 --hash=sha256:...
Defensive patterns
Strategy: validation
Validate before calling
import re
from pip._vendor.packaging.requirements import Requirement
def all_pinned_for_hashes(req_file: str) -> tuple[bool, list[str]]:
bad = []
for line in open(req_file):
line = line.split('#')[0].strip()
if not line:
continue
try:
r = Requirement(line)
except Exception:
continue
if r.url: # direct/VCS URLs are exempt as direct requirements
continue
pinned = any(op == '==' and not spec.version.endswith('.*')
for op, spec in zip(r.specifier.operator, r.specifier.version) for _ in [0])
# Simpler robust check: look for == in the specifier
pinned = any(s.operator == '==' for s in r.specifier)
if not pinned:
bad.append(line)
return (not bad, bad) Type guard
from pip._vendor.packaging.requirements import Requirement
def is_pinned_exact(line: str) -> bool:
try:
r = Requirement(line.split(' --')[0].strip())
except Exception:
return False
if r.url:
return True # direct URLs are exempt
return any(s.operator == '==' for s in r.specifier) Try / catch
# Use pip-compile to produce a fully pinned+hashed file rather than hand-editing. from subprocess import run run(["pip-compile", "--generate-hashes", "requirements.in", "-o", "requirements.txt"], check=True)
Prevention
- Generate hashed requirements files with pip-compile --generate-hashes.
- Audit requirement files for unpinned (non-==) lines before enabling --require-hashes.
- Treat --require-hashes as all-or-nothing across the install set.
When it happens
Trigger: A requirements file passed with --require-hashes containing a line without an == specifier (e.g. 'requests' or 'requests>=2.0'). Checked via req.is_pinned being False for indirect requirements.
Common situations: Adopting --require-hashes partway through a project; a requirements file that mixes pinned-and-hashed lines with loose dependency lines; transitive deps pulled in without pins when hash mode is on.
Related errors
- Can't verify hashes for these file:// requirements because…
- Can't verify hashes for these requirements because we don't…
- The editable requirement
- Packages installed from PyPI cannot depend on packages…
- THESE PACKAGES DO NOT MATCH THE HASHES FROM THE…
AI-assisted analysis of pypa/pip@f399c37189 (2026-08-08).
Data as JSON: /api/errors/d93995f322538bd7.
Report an issue: GitHub.
Appendix: source
Thrown at src/pip/_internal/operations/prepare.py:484
if not self.require_hashes:
return req.hashes(trust_internet=True)
# We could check these first 2 conditions inside unpack_url
# and save repetition of conditions, but then we would
# report less-useful error messages for unhashable
# requirements, complaining that there's no hash provided.
if req.link.is_vcs:
raise VcsHashUnsupported()
if req.link.is_existing_dir():
raise DirectoryUrlHashUnsupported()
# Unpinned packages are asking for trouble when a new version
# is uploaded. This isn't a security check, but it saves users
# a surprising hash mismatch in the future.
# file:/// URLs aren't pinnable, so don't complain about them
# not being pinned.
if not req.is_direct and not req.is_pinned:
raise HashUnpinned()
# If known-good hashes are missing for this requirement,
# shim it with a facade object that will provoke hash
# computation and then raise a HashMissing exception
# showing the user what the hash should be.
return req.hashes(trust_internet=False) or MissingHashes()
def _fetch_metadata_only(
self,
req: InstallRequirement,
) -> BaseDistribution | None:
if self.legacy_resolver:
logger.debug(
"Metadata-only fetching is not used in the legacy resolver",
)
return None
if self.require_hashes:
logger.debug(View on GitHub (pinned to f399c37189)