Skip to content

set/frozenset re-hash elements when constructed from a set, frozenset, or dict #8489

Description

@fregataa

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:333TestJointOps.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)

Metadata

Metadata

Assignees

No one assigned

    Labels

    z-ca-2026Tag to track Contribution Academy 2026

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions