2025, Sep 24 11:00

Is Python Set Iteration Order Stable? Within-Run Guarantees, Cross-Run Hash Randomization, and Reproducible Snapshots

Python set iteration order stays stable within a run; hash randomization changes it across runs. Use PYTHONHASHSEED or list snapshots for reproducible results.

Python sets are mutable and explicitly unordered. This is well known, but a practical question often follows: if you iterate a set twice during the same program run, without modifying it in between, can the iteration order drift because of internal rehashing or some hidden allocator magic? Or, put differently, is the iteration order of an unchanged set stable within a single execution?

Reproducing the concern

The snippet below captures the essence of the question. A set is created, one pass is recorded, and later we check whether a second pass yields the same sequence.

bag = {1, 4, 3, 5}
snapshot = [x for x in bag]
# other logic that does not touch the set
assert [x for x in bag] == snapshot

What actually happens

According to the Python glossary, a hashable object has a hash value that never changes during its lifetime. The standard library documentation for set states that set elements must be hashable. Put these two together and the picture becomes clear: a set’s iteration is determined by the hash table layout built from element hashes, and those hashes do not change during the life of the objects. As a result, a set that is not modified will not be internally rehashed or reallocated by the interpreter during the same run, and its iteration order will stay the same within that execution.

Lists preserve insertion order. When you turn a set into a list, the list reflects the order produced by the set at that moment, and repeating the conversion later – while the set remains untouched – yields the same list order during the same run.

The solution in practice

If your code needs a stable snapshot of a set’s iteration order within a single run, take the snapshot and reuse it. The assertion below will hold as long as the set remains unchanged.

pool = {1, 4, 3, 5}
first_pass = list(pool)
# unrelated work that does not mutate `pool`
assert list(pool) == first_pass

About cross-run variability

Across separate interpreter executions, Python randomizes hashes. That means the iteration order for the same set values may differ from run to run. You can see this by building a list filtered by an object’s hash, converting it into a set, and then back to a list. The list preserves the set’s order, and that order may vary across executions due to hash randomization and hash collisions.

for i in 1 2 3 4; do
  python3.12 -c 'import string as alpha; seq = [f"ba{ch}" for ch in alpha.ascii_lowercase if hash(f"ba{ch}") % 8 == 2]; print(f"{seq}\n{set(seq)}\n{list(set(seq))}\n")'
done

Result

['baa', 'bah', 'bai', 'bam', 'bap']
{'baa', 'bap', 'bah', 'bai', 'bam'}
['baa', 'bap', 'bah', 'bai', 'bam']
['bag', 'bai', 'bal', 'bax']
{'bai', 'bag', 'bal', 'bax'}
['bai', 'bag', 'bal', 'bax']
['bae', 'bai', 'baq', 'bax']
{'baq', 'bae', 'bax', 'bai'}
['baq', 'bae', 'bax', 'bai']
['bae', 'bap', 'bax']
{'bae', 'bap', 'bax'}
['bae', 'bap', 'bax']

If you need reproducible ordering across runs, you can turn off hash randomization by setting PYTHONHASHSEED to a constant value. With a fixed seed, the order is consistent from execution to execution.

for i in 1 2 3 4; do
  PYTHONHASHSEED=1 \
  python3.12 -c 'import string as alpha; seq = [f"ba{ch}" for ch in alpha.ascii_lowercase if hash(f"ba{ch}") % 8 == 2]; print(f"{seq}\n{set(seq)}\n{list(set(seq))}\n")'
done

Result

['bac', 'bah', 'bak', 'baw']
{'bac', 'bak', 'bah', 'baw'}
['bac', 'bak', 'bah', 'baw']
['bac', 'bah', 'bak', 'baw']
{'bac', 'bak', 'bah', 'baw'}
['bac', 'bak', 'bah', 'baw']
['bac', 'bah', 'bak', 'baw']
{'bac', 'bak', 'bah', 'baw'}
['bac', 'bak', 'bah', 'baw']
['bac', 'bah', 'bak', 'baw']
{'bac', 'bak', 'bah', 'baw'}
['bac', 'bak', 'bah', 'baw']

Why this matters

Understanding the iteration behavior of sets helps avoid subtle bugs. Within a single run, if a set is not modified, relying on a previously observed order is safe. Across runs, however, the order is not portable unless you disable hash randomization. This difference is crucial when writing tests, serializing data derived from sets, or comparing outputs from multiple executions.

Takeaways

If you iterate a set multiple times during the same program execution without mutating it, the order will remain stable during that run. Lists preserve the order they receive from the set at the time of conversion, so you can snapshot once and reuse. Expect differences between separate executions because Python randomizes hashes; if you need reproducible order across runs for debugging or testing, set PYTHONHASHSEED to a constant.

The article is based on a question from StackOverflow by SLebedev777 and an answer by LMC.