pypa/pip · error · DirectUrlValidationError

Algorithm used in hash field is not present in hashes field

Error message

Algorithm {hash_algorithm!r} used in hash field is not present in hashes field

What it means

When both the legacy hash field and the newer hashes mapping are present in archive_info, they must be consistent. The algorithm extracted from the legacy hash (the part before =) must also be a key in the hashes mapping. If it is missing, DirectUrlValidationError is raised because the two hash representations disagree about which algorithms are available.

Solutions

  1. Ensure the algorithm in the legacy hash matches one of the keys in hashes
  2. Remove the legacy hash field and rely solely on hashes
  3. Regenerate the metadata from the correct source using to_dict()

Example fix

# before
data = {
    "url": "https://example.com/pkg.tar.gz",
    "archive_info": {
        "hash": "md5=abc123",
        "hashes": {"sha256": "def456"}  # md5 not in hashes
    }
}

# after
data = {
    "url": "https://example.com/pkg.tar.gz",
    "archive_info": {"hashes": {"sha256": "def456"}}  # drop legacy hash
}
Defensive patterns

Strategy: validation

Validate before calling

def validate_hash_consistency(archive_info: dict) -> None:
    legacy = archive_info.get("hash")
    hashes = archive_info.get("hashes")
    if legacy and hashes:
        algo = legacy.split("=", 1)[0]
        if algo not in hashes:
            raise ValueError(f"Algorithm {algo!r} from hash not in hashes")

Type guard

def are_hashes_consistent(archive_info: dict) -> bool:
    legacy = archive_info.get("hash")
    hashes = archive_info.get("hashes")
    if not legacy or not hashes:
        return True
    return legacy.split("=", 1)[0] in hashes

Try / catch

from packaging.direct_url import DirectUrl, DirectUrlValidationError

try:
    du = DirectUrl.from_dict(data)
except DirectUrlValidationError as e:
    if "not present in hashes" in str(e):
        data["archive_info"].pop("hash", None)
        du = DirectUrl.from_dict(data)

Prevention

When it happens

Trigger: An archive_info block with both hash and hashes where the algorithm in hash does not appear in hashes: e.g. {'hash': 'md5=abc', 'hashes': {'sha256': 'def'}}.

Common situations: Metadata generated by tools that write both fields with different algorithms. Migration artifacts where the legacy field was not updated alongside the new field.

Related errors


AI-assisted analysis of pypa/pip@f399c37189 (2026-08-08). Data as JSON: /api/errors/7cbfa0df22038088. Report an issue: GitHub.

Appendix: source

Thrown at src/pip/_vendor/packaging/direct_url.py:218

        if hashes is not None and not all(isinstance(h, str) for h in hashes.values()):
            raise DirectUrlValidationError(
                "Hash values must be strings", context="hashes"
            )
        legacy_hash = _get(d, str, "hash")
        if legacy_hash is not None:
            if "=" not in legacy_hash:
                raise DirectUrlValidationError(
                    "Invalid hash format (expected '<algorithm>=<hash>')",
                    context="hash",
                )
            hash_algorithm, hash_value = legacy_hash.split("=", 1)
            if hashes is None:
                # if `hashes` are not present, we can derive it from the legacy `hash`
                hashes = {hash_algorithm: hash_value}
            else:
                # if `hashes` are present, the legacy `hash` must match one of them
                if hash_algorithm not in hashes:
                    raise DirectUrlValidationError(
                        f"Algorithm {hash_algorithm!r} used in hash field "
                        f"is not present in hashes field",
                        context="hashes",
                    )
                if hashes[hash_algorithm] != hash_value:
                    raise DirectUrlValidationError(
                        f"Algorithm {hash_algorithm!r} used in hash field "
                        f"has different value in hashes field",
                        context="hash",
                    )
        return cls(hashes=hashes)


@dataclasses.dataclass(frozen=True, init=False)
class DirInfo:
    """The local directory information of a :class:`DirectUrl`."""

    editable: bool | None = None

View on GitHub (pinned to f399c37189)