skip to content

Can you rely on the iteration order of a Python set or frozenset?

level: seniorimportance: nice to knowfreq 22%

answer

  1. Is there a first element?
  2. Neither type defines __getitem__
  3. Order is hash-table slot order
  4. String hashes are salted per process
  5. PYTHONHASHSEED pins it for debugging

basics

~20 s

No. Sets and frozensets are unordered and not subscriptable; iteration follows hash-table slot order, which depends on element hashes and insertion history. For str elements the hash is salted per process, so order can differ between runs of one script.

solid answer

~50 s

A `set` or `frozenset` has no defined order and no `__getitem__` — `fs[0]` raises `TypeError: 'frozenset' object is not subscriptable`. Iteration yields the hash table's slot order, which falls out of each element's hash, the current table size and the history of insertions and removals. For integers that layout is repeatable. For `str` and `bytes` it is not: CPython salts string hashing with a per-process seed, so `list({'alpha', 'beta', 'gamma'})` can print a different order in two runs of the same script. The salt exists to blunt hash-collision denial-of-service attacks; setting the `PYTHONHASHSEED` environment variable to a fixed value pins it, which is a debugging aid rather than a design. If order matters, impose it at the point of use with `sorted()` or `min()`, and never let a test, a log line or a serialized payload depend on set order.

code

console · 2 lines
console
python3.14 -c "print(list({'alpha', 'beta', 'gamma', 'delta'}))"
python3.14 -c "print(list({'alpha', 'beta', 'gamma', 'delta'}))"

go deeper

for a junior

Know that sets and frozensets are unordered and cannot be indexed, and that sorted() is how you get a predictable sequence out of one. That much is enough at this level.

for a middle

Explain where the order comes from — element hashes, table size, insertion history — and that str hashing is salted per process, so two runs of one script can print different orders.

for a senior

Show the production instinct: never let a test assertion, log line or serialized payload depend on set order, and treat a pinned PYTHONHASHSEED as a debugging tool rather than a fix for flaky output.

for a principal

Own the reproducibility policy: where determinism is contractually required, how it is enforced by canonicalizing at the boundary, and why weakening hash randomization to buy stable order trades a security property for convenience.

### There is no order to rely on A `set` and a `frozenset` are hash tables with no auxiliary ordering structure. They implement no `__getitem__`, so `fs[0]` raises `TypeError: 'frozenset' object is not subscriptable`, and there is no `.first()`, no slicing and no reverse iteration. Iterating one walks the internal table from slot zero upward and yields whatever is parked in each occupied slot. That order is a function of three things: the hash of each element, the current table size (which grows in powers of two as the set fills), and the history of insertions and deletions that produced the current layout. None of those are part of the language contract. Two sets that compare equal can iterate in different orders — build `{1, 2, 3}` by adding in one sequence and another by adding in the reverse, and after enough resizes their layouts can differ while `==` still reports `True`. ### The part that surprises people: it varies between runs For `int` elements, CPython hashes a small integer to itself, so the layout is reproducible run after run. For `str` and `bytes`, it is not. CPython salts string hashing with a per-process random seed, so the same script can print different orders on consecutive runs: ```console $ python3.14 -c "print(list({'alpha', 'beta', 'gamma', 'delta'}))" ['alpha', 'gamma', 'beta', 'delta'] $ python3.14 -c "print(list({'alpha', 'beta', 'gamma', 'delta'}))" ['delta', 'beta', 'alpha', 'gamma'] ``` This is deliberate. Without a salt, an attacker who controls the string keys reaching a hash table can craft a set of inputs that all collide into one bucket, degrading lookups from near-constant to linear and turning a modest request into a denial of service. Randomizing the seed makes those collision sets unpredictable per process. The price is that string-keyed hash layout is not stable across processes. Setting the `PYTHONHASHSEED` environment variable to a fixed integer disables the randomization and makes the order repeatable. That is a legitimate debugging move — pin the seed, reproduce the odd ordering, understand the bug — and a poor production setting, because you are trading a security property for a convenience you should not have been depending on in the first place. ### What breaks when you depend on it The failures all have the same shape: something passes locally and fails elsewhere, or passes a thousand times and fails once. * A test asserting `list(result_set) == ['a', 'b', 'c']` passes on your machine and fails in CI. * A log line or an error message that lists a set renders its items in a different order every restart, so diffing two runs is useless. * A serialized payload built from a set produces a different byte string per process, breaking any downstream checksum, cache key or golden-file comparison. * A "pick one" step written as `next(iter(candidates))` chooses a different candidate per process, which is fine until the choice is user-visible or must be reproducible. ### Doing it properly If order matters, impose it explicitly at the point of use. `sorted(fs)` returns a **list** in an order you defined — note that it is not a set, and that it costs O(n log n) each time. `min(fs)` or `max(fs)` gets a deterministic single element without a full sort. If the order is intrinsic to the data rather than to the presentation, the set is the wrong structure: keep an ordered container and use a set only as a companion membership index. `next(iter(fs))` is the right way to grab *an* element when genuinely any member will do — it is cheap and honest about being arbitrary. It is the wrong way the moment a log, a test or a response body records which one was chosen. ### One contrast worth stating explicitly `dict` and `set` are both hash tables, and people carry a rule from one to the other that does not apply. Dicts have a documented insertion-order guarantee; sets have never had one and still do not on 3.14. The randomization itself is not new either — it has been on by default since 3.3. If your mental model is "Python containers remember order now", sets are the exception that will eventually cost you an afternoon.

  • How do you get a deterministic single element out of a frozenset?
    Decide what you mean by first and impose it: `min(fs)` or `sorted(fs)[0]` under an order you define, or keep the ordering in a separate list and use the set only for membership. `next(iter(fs))` gives *an* element cheaply, which is right when any member will do and wrong the moment a log line, a test assertion or a response body records which one was picked.
  • Why is hash randomization on by default at all?
    It defends against algorithmic complexity attacks. Without a per-process salt, an attacker who controls string keys can craft inputs that all collide into one bucket, degrading near-constant lookups to linear and collapsing a service under modest traffic. Randomizing the seed makes those collision sets unpredictable per process; the cost is that string-keyed hash layout is no longer stable across processes.
  • Is the iteration order of a set of small integers stable across runs?
    In CPython, yes — a small int hashes to itself, so no randomization applies and identically-built tables lay out identically. That stability is an implementation detail, not a promise: a different build, or a different history of insertions and removals, can reorder it. Code that leans on it is still broken code that happens to pass.

saying these in an interview costs you the question

  • Says sets preserve insertion order like dicts
  • Uses fs[0] to grab an element
  • Assumes set order is stable across processes
  • Thinks sorted() on a set returns a set
  • Pins PYTHONHASHSEED in production to fix ordering

context