Skip to content

Fix NamedTuple hash returning 0 on Python 3.14+#980

Open
worksbyfriday wants to merge 1 commit intojcrist:mainfrom
worksbyfriday:fix-namedtuple-hash-py314
Open

Fix NamedTuple hash returning 0 on Python 3.14+#980
worksbyfriday wants to merge 1 commit intojcrist:mainfrom
worksbyfriday:fix-namedtuple-hash-py314

Conversation

@worksbyfriday
Copy link

Summary

Fixes #967.

In Python 3.14, CPython added hash caching to tuples (gh-131525, PR #131529). PyTupleObject gained an ob_hash field initialized to -1 ("not yet computed"). When tuple_hash() sees ob_hash != -1, it returns the cached value immediately.

msgspec allocates NamedTuples via tp_alloc, which zero-initializes memory. This sets ob_hash to 0 — a valid cached hash value — so hash() returns 0 for all msgspec-deserialized NamedTuples without ever computing the real hash. This breaks dictionary lookups and set membership when deserialized NamedTuples are used as keys.

The fix adds MS_TUPLE_RESET_HASH(op) — a macro that sets ob_hash = -1 on Python 3.14+ (no-op on earlier versions) — immediately after tp_alloc in all three NamedTuple construction sites:

  • mpack_decode_namedtuple (msgpack decoder)
  • json_decode_namedtuple (JSON decoder)
  • convert_seq_to_namedtuple (convert)

Test plan

  • Added test_namedtuple_hash_preserved_after_roundtrip in test_common.py (covers msgpack + JSON)
  • Added test_namedtuple_hash_preserved in test_convert.py (covers convert path)
  • Both tests verify hash(original) == hash(decoded) and dict lookup with decoded keys
  • All 32 existing NamedTuple tests pass on Python 3.12 (macro is no-op)
  • The fix is effective on Python 3.14+ where ob_hash exists in PyTupleObject

In Python 3.14, CPython added hash caching to tuples (gh-131525).
PyTupleObject gained an ob_hash field that is checked by tuple_hash()
before computing: if ob_hash != -1, the cached value is returned
immediately.

When msgspec allocates NamedTuples via tp_alloc (which zero-initializes
memory via calloc), ob_hash is set to 0 — a valid cached hash value.
This causes hash() to return 0 for all msgspec-deserialized NamedTuples,
breaking their use as dictionary keys and in sets.

The fix resets ob_hash to -1 ("not yet computed") immediately after
tp_alloc in all three NamedTuple construction sites: msgpack decoder,
JSON decoder, and convert. The reset is conditional on PY314_PLUS and
compiles to a no-op on earlier Python versions.

Fixes jcrist#967
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

NamedTuple hash changes during round trip msgspec deserialisation in python 3.14

1 participant