Summary
set/frozenset recompute __hash__ for every element when the source is a set, frozenset, or dict. CPython reuses the hash already stored in the source's entries (set_update_internal in Objects/setobject.c, branching on PyAnySet_Check / PyDict_CheckExact), so no extra __hash__ calls happen.
Scope
The set/frozenset target only. The dict target (dict.fromkeys) is #8490. test_do_not_rehash_dict_keys asserts on both directions, so its @unittest.expectedFailure # TODO: RUSTPYTHON marker can only be dropped once both are done.
Reproduction
class HashCountingInt(int):
def __init__(self, *args):
self.hash_count = 0
def __hash__(self):
self.hash_count += 1
return int.__hash__(self)
d = dict.fromkeys(map(HashCountingInt, range(10)))
print(sum(e.hash_count for e in d)) # 10
frozenset(d)
print(sum(e.hash_count for e in d)) # CPython: 10, RustPython: 20
Lib/test/test_set.py:333 — TestJointOps.test_do_not_rehash_dict_keys, inherited by TestSet, TestSetSubclass, TestFrozenSet and TestFrozenSetSubclass (4 of the 10 expected failures in test_set.py).
Analysis
The hash is already stored — DictEntry keeps it next to the key (crates/vm/src/dict_inner.rs:227), and PySetInner::update_internal (crates/vm/src/builtins/set.rs:379) already special-cases AnySet and PyDict. But both branches drop it and go through self.add() → content.insert(), which re-hashes:
fn merge_set(&self, any_set: AnySet, vm: &VirtualMachine) -> PyResult<()> {
for item in any_set.as_inner().elements() {
self.add(item, vm)?; // recomputes the hash
}
Ok(())
}
update_internal is also only reached from set.update() (:719). These paths never dispatch on the source type at all:
PySetInner::from_iter (:187), used by the set() / frozenset() constructors
union (:252), intersection (:261), difference (:272), symmetric_difference (:280)
intersection_update (:409), difference_update (:422), symmetric_difference_update (:436)
Summary
set/frozensetrecompute__hash__for every element when the source is aset,frozenset, ordict. CPython reuses the hash already stored in the source's entries (set_update_internalinObjects/setobject.c, branching onPyAnySet_Check/PyDict_CheckExact), so no extra__hash__calls happen.Scope
The set/frozenset target only. The
dicttarget (dict.fromkeys) is #8490.test_do_not_rehash_dict_keysasserts on both directions, so its@unittest.expectedFailure # TODO: RUSTPYTHONmarker can only be dropped once both are done.Reproduction
Lib/test/test_set.py:333—TestJointOps.test_do_not_rehash_dict_keys, inherited byTestSet,TestSetSubclass,TestFrozenSetandTestFrozenSetSubclass(4 of the 10 expected failures intest_set.py).Analysis
The hash is already stored —
DictEntrykeeps it next to the key (crates/vm/src/dict_inner.rs:227), andPySetInner::update_internal(crates/vm/src/builtins/set.rs:379) already special-casesAnySetandPyDict. But both branches drop it and go throughself.add()→content.insert(), which re-hashes:update_internalis also only reached fromset.update()(:719). These paths never dispatch on the source type at all:PySetInner::from_iter(:187), used by theset()/frozenset()constructorsunion(:252),intersection(:261),difference(:272),symmetric_difference(:280)intersection_update(:409),difference_update(:422),symmetric_difference_update(:436)