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

  1. Generate a fully pinned+hashed requirements file with 'pip-compile --generate-hashes' (pip-tools).
  2. Pin each unpinned requirement to an exact version with == and add its sha256 hash.
  3. 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

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


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)