pypa/pip · error · DirectUrlValidationError

Algorithm used in hash field has different value in hashes…

Error message

Algorithm {hash_algorithm!r} used in hash field has different value in hashes field

What it means

When both the legacy hash field and the hashes mapping exist and the algorithm from hash is present in hashes, the digest values must match exactly. If they differ, DirectUrlValidationError is raised, indicating a hash conflict that could signal tampering or corruption.

Solutions

  1. Recompute the correct hash from the actual file and update both fields (or just hashes)
  2. Remove the legacy hash field to eliminate the conflict
  3. Investigate if the conflict is unexpected as it may indicate file corruption or tampering

Example fix

# before
data = {
    "url": "https://example.com/pkg.tar.gz",
    "archive_info": {
        "hash": "sha256=old_value",
        "hashes": {"sha256": "new_value"}  # mismatched
    }
}

# after
import hashlib
correct = hashlib.sha256(file_bytes).hexdigest()
data = {
    "url": "https://example.com/pkg.tar.gz",
    "archive_info": {"hashes": {"sha256": correct}}
}
Defensive patterns

Strategy: validation

Validate before calling

def validate_hash_value_match(archive_info: dict) -> None:
    legacy = archive_info.get("hash")
    hashes = archive_info.get("hashes")
    if legacy and hashes:
        algo, digest = legacy.split("=", 1)
        if algo in hashes and hashes[algo] != digest:
            raise ValueError(f"Hash mismatch for {algo!r}: legacy={digest!r} vs hashes={hashes[algo]!r}")

Type guard

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

Try / catch

from packaging.direct_url import DirectUrl, DirectUrlValidationError

try:
    du = DirectUrl.from_dict(data)
except DirectUrlValidationError as e:
    if "different value" in str(e):
        data["archive_info"].pop("hash")
        du = DirectUrl.from_dict(data)

Prevention

When it happens

Trigger: An archive_info block where the same algorithm appears in both hash and hashes with different digest values: e.g. {'hash': 'sha256=aaa', 'hashes': {'sha256': 'bbb'}}.

Common situations: Metadata edited by hand or by buggy tools. Hash recomputation that updated one field but not the other. Potential supply-chain integrity concern if the conflict is unexpected.

Related errors


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

Appendix: source

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

            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

    def __init__(
        self,
        *,
        editable: bool | None = None,
    ) -> None:

View on GitHub (pinned to f399c37189)