{"id":"37abd6f72a5834bc","repo":"pypa/pip","slug":"hash-values-must-be-strings","errorCode":null,"errorMessage":"Hash values must be strings","messagePattern":"Hash values must be strings","errorType":"validation","errorClass":"DirectUrlValidationError","httpStatus":null,"severity":"error","filePath":"src/pip/_vendor/packaging/direct_url.py","lineNumber":189,"sourceCode":"        )\n\n\n@dataclasses.dataclass(frozen=True, init=False)\nclass ArchiveInfo:\n    hashes: Mapping[str, str] | None = None\n\n    def __init__(\n        self,\n        *,\n        hashes: Mapping[str, str] | None = None,\n    ) -> None:\n        object.__setattr__(self, \"hashes\", hashes)\n\n    @classmethod\n    def _from_dict(cls, d: Mapping[str, Any]) -> Self:\n        hashes = _get(d, Mapping, \"hashes\")  # type: ignore[type-abstract]\n        if hashes is not None and not all(isinstance(h, str) for h in hashes.values()):\n            raise DirectUrlValidationError(\n                \"Hash values must be strings\", context=\"hashes\"\n            )\n        legacy_hash = _get(d, str, \"hash\")\n        if legacy_hash is not None:\n            if \"=\" not in legacy_hash:\n                raise DirectUrlValidationError(\n                    \"Invalid hash format (expected '<algorithm>=<hash>')\",\n                    context=\"hash\",\n                )\n            hash_algorithm, hash_value = legacy_hash.split(\"=\", 1)\n            if hashes is None:\n                # if `hashes` are not present, we can derive it from the legacy `hash`\n                hashes = {hash_algorithm: hash_value}\n            else:\n                # if `hashes` are present, the legacy `hash` must match one of them\n                if hash_algorithm not in hashes:\n                    raise DirectUrlValidationError(\n                        f\"Algorithm {hash_algorithm!r} used in hash field \"","sourceCodeStart":171,"sourceCodeEnd":207,"githubUrl":"https://github.com/pypa/pip/blob/d7d0d0a39494e28ec1c407bd0680e4a4d1067791/src/pip/_vendor/packaging/direct_url.py#L171-L207","documentation":"Raised by `ArchiveInfo._from_dict` when the `hashes` mapping is present but at least one of its values is not a string. Per PEP 610, `hashes` is `Mapping[str, str]` (algorithm → hex digest); non-string values violate the schema and are rejected.","triggerScenarios":"A `direct_url.json` where `archive_info.hashes` contains a value that is a number, list, or nested object, e.g. `{\"sha256\": 12345}` or `{\"md5\": [\"abc\"]}`.","commonSituations":"A custom installer or build backend that serializes hashes with the wrong JSON type; a JSON produced by code that wrote `hashlib.sha256(...).digest()` (bytes) instead of `.hexdigest()` (str); schema drift between tools.","solutions":["Regenerate the `direct_url.json` via pip reinstall","Ensure hash producers call `.hexdigest()` (str), not `.digest()` (bytes), and never numeric hashes","Validate `all(isinstance(v, str) for v in hashes.values())` before constructing ArchiveInfo"],"exampleFix":"// before\nimport hashlib\nhashes = {'sha256': hashlib.sha256(data).digest()}  # bytes\n// after\nhashes = {'sha256': hashlib.sha256(data).hexdigest()}  # str","handlingStrategy":"type-guard","validationCode":"def validate_hashes(hashes: dict) -> None:\n    if hashes is not None and not all(isinstance(v, str) for v in hashes.values()):\n        raise TypeError('all hash values must be str (hex digest)')","typeGuard":"def is_str_value_hashes(h: object) -> bool:\n    return h is None or (isinstance(h, dict) and all(isinstance(v, str) for v in h.values()))","tryCatchPattern":"from packaging.direct_url import ArchiveInfo, DirectUrlValidationError\ntry:\n    ai = ArchiveInfo._from_dict(d)\nexcept DirectUrlValidationError as e:\n    if 'Hash values must be strings' in str(e):\n        d['archive_info']['hashes'] = {k: str(v) for k, v in d['archive_info']['hashes'].items()}\n    raise","preventionTips":["Always produce hashes via .hexdigest(), never .digest() or numeric","Type-check hashes.values() before serialization","Reinstall packages to regenerate clean direct_url.json"],"tags":["packaging","direct-url","pep610","hashes"],"analyzedSha":"d7d0d0a39494e28ec1c407bd0680e4a4d1067791","analyzedAt":"2026-08-04T20:55:04.259Z","schemaVersion":2}