pypa/pip · error · DirectUrlValidationError

Hash values must be strings

Error message

Hash values must be strings

What it means

In ArchiveInfo._from_dict(), the hashes field is expected to be a mapping of algorithm names to hash digest strings (e.g. {'sha256': 'abc...'}). If any value in the hashes mapping is not a string, DirectUrlValidationError is raised with context 'hashes'. This enforces PEP 610 / direct URL spec compliance for hash integrity data.

Solutions

  1. Ensure all hash values in the hashes mapping are strings
  2. Validate the hashes dict structure before parsing
  3. Regenerate the direct_url.json using the standard to_dict() method

Example fix

# before
data = {
    "url": "https://example.com/pkg.tar.gz",
    "archive_info": {"hashes": {"sha256": 123456}}  # int
}

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

Strategy: validation

Validate before calling

def validate_hashes_field(archive_info: dict) -> None:
    hashes = archive_info.get("hashes")
    if hashes is not None:
        if not isinstance(hashes, dict):
            raise TypeError("hashes must be a dict")
        for algo, digest in hashes.items():
            if not isinstance(digest, str):
                raise TypeError(f"hash value for {algo!r} must be a string, got {type(digest).__name__}")

Type guard

def is_valid_hashes(hashes) -> bool:
    return hashes is None or (
        isinstance(hashes, dict)
        and all(isinstance(k, str) and isinstance(v, str) for k, v in hashes.items())
    )

Try / catch

from packaging.direct_url import DirectUrl, DirectUrlValidationError

try:
    du = DirectUrl.from_dict(data)
except DirectUrlValidationError as e:
    if "Hash values must be strings" in str(e):
        # fix hash types in data and retry
        pass

Prevention

When it happens

Trigger: Parsing an archive_info block from direct_url.json where hashes values are non-string: e.g. {'hashes': {'sha256': 12345}} or {'hashes': {'sha256': {'digest': 'abc'}}}.

Common situations: Incorrectly generated metadata by custom build tools, hand-crafted JSON, or schema drift from non-standard packaging tooling that stores hash digests as numbers or nested objects.

Related errors


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

Appendix: source

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

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

    hashes: Mapping[str, str] | None = None

    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 "

View on GitHub (pinned to f399c37189)