pypa/pip · error · DirectUrlValidationError

Invalid hash format (expected ' = ')

Error message

Invalid hash format (expected '<algorithm>=<hash>')

What it means

The legacy hash field in archive_info (from older PEP 610 implementations) must follow the format <algorithm>=<hash_digest>, e.g. sha256=abc123.... If the string lacks an = sign, DirectUrlValidationError is raised. This is separate from the newer hashes mapping field.

Solutions

  1. Format the legacy hash as <algorithm>=<digest> with an equals sign separator
  2. Prefer the newer hashes mapping field over the legacy hash string
  3. Remove the legacy hash field if hashes is already present and correct

Example fix

# before
data = {
    "url": "https://example.com/pkg.tar.gz",
    "archive_info": {"hash": "abc123def456"}  # missing algorithm=
}

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

Strategy: validation

Validate before calling

def validate_legacy_hash(archive_info: dict) -> None:
    legacy = archive_info.get("hash")
    if legacy is not None:
        if "=" not in legacy:
            raise ValueError(f"Legacy hash must be 'algorithm=digest', got: {legacy!r}")
        algo, digest = legacy.split("=", 1)
        if not algo or not digest:
            raise ValueError(f"Invalid legacy hash: {legacy!r}")

Type guard

def is_valid_legacy_hash(h) -> bool:
    if not isinstance(h, str):
        return False
    parts = h.split("=", 1)
    return len(parts) == 2 and all(parts)

Try / catch

from packaging.direct_url import DirectUrl, DirectUrlValidationError

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

Prevention

When it happens

Trigger: Parsing an archive_info block that has a legacy hash field without an = separator: e.g. {'hash': 'abc123'} or {'hash': 'sha256:'} (colon instead of equals).

Common situations: Migrating from older metadata formats. Custom build scripts that write hash fields with the wrong separator. Tooling that uses a colon instead of equals sign.

Related errors


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

Appendix: source

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

    def __init__(
        self,
        *,
        hashes: Mapping[str, str] | None = None,
    ) -> None:
        object.__setattr__(self, "hashes", hashes)

    @classmethod
    def _from_dict(cls, d: Mapping[str, Any]) -> Self:
        hashes = _get(d, Mapping, "hashes")  # type: ignore[type-abstract]
        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 "

View on GitHub (pinned to f399c37189)