Skip to content

NamedTuple hash changes during round trip msgspec deserialisation in python 3.14 #967

@rshanker779

Description

@rshanker779

Description

Hey,
I saw this behaviour in msgspec 0.20.0 and Python 3.14.1.
NamedTuples have hash changes after being serialised, and deserialised with msgpack- the hash becomes 0.
This means for msgspec structs where NamedTuples are used as dictionary keys, there is unexpected KeyErrors when accessing data

Here is a minimal examples of the issue- the following code throws no AssertionErrors in python 3.11-3.13

from typing import NamedTuple
import msgspec

class _TestKey(NamedTuple):
    """Test NamedTuple for demonstrating the msgspec Python 3.14 bug."""

    value: int


class _TestStruct(msgspec.Struct, array_like=True):
    """Test Struct that contains a dict with NamedTuple keys."""

    data: dict[_TestKey, str]


def test_msgspec_namedtuple_hash_bug():
    original = _TestKey(value=42)
    original_hash = hash(original)

    # Round-trip through msgspec - hash is broken in Python 3.14
    encoded = msgspec.msgpack.encode(original)
    decoded = msgspec.msgpack.decode(encoded, type=_TestKey)

    # These should be equal
    assert original == decoded, "NamedTuples should be equal after round-trip"

    # This is the bug: hash should be equal but isn't in Python 3.14
    decoded_hash = hash(decoded)
    assert decoded_hash == original_hash, (
        f"Hash mismatch after msgspec round-trip: original={original_hash}, decoded={decoded_hash}. "
        f"In Python 3.14, msgspec-created NamedTuples have hash=0."
    )

    # Dictionary lookup fails due to hash mismatch
    test_dict = {original: "value"}
    assert decoded in test_dict, "Decoded key should be found in dictionary (requires matching hash)"


def test_msgspec_struct_with_namedtuple_dict_key_bug():
    """
    Demonstrates the msgspec Python 3.14 bug when a NamedTuple is used as a dictionary
    key inside a msgspec.Struct - this mirrors the real-world usage in CalculationMetadata
    where SourceMarketKey is used as a key in base_inputs.

    After msgpack deserialization:
    - The Struct is correctly deserialized
    - The dict appears to contain the expected key
    - But dict equality fails because the deserialized keys have hash=0
    - Looking up the key also fails due to hash mismatch
    """
    key = _TestKey(value=42)
    original = _TestStruct(data={key: "test_value"})

    # Round-trip through msgspec
    encoded = msgspec.msgpack.encode(original)
    decoded = msgspec.msgpack.decode(encoded, type=_TestStruct)

    # Get the decoded key for hash comparison
    decoded_key = next(iter(decoded.data.keys()))
    original_key_hash = hash(key)
    decoded_key_hash = hash(decoded_key)

    # The struct should be equal - this is the assertion that fails
    assert original == decoded, (
        f"Struct with NamedTuple dict keys should be equal after round-trip. "
        f"This fails because dict comparison fails due to hash mismatch on keys. "
        f"original_key_hash={original_key_hash}, decoded_key_hash={decoded_key_hash}"
    )

    # Verify the key can be looked up in the decoded struct's dict
    assert key in decoded.data, (
        f"Original key should be found in decoded dict. "
        f"original_key_hash={original_key_hash}, decoded_key_hash={decoded_key_hash}"
    )
    assert decoded.data[key] == "test_value", "Value should be retrievable with original key"

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions