skip to content

Insertion Order and Layout

CPython's dict is a compact hash table — a sparse index array over dense entries — which is why insertion order became a guarantee in 3.7. Interviewers ask where that ordering actually comes from.

part ofPythonoverview, primer and where to startread it →
on this pageshow

questions

4

Since which Python version is dict insertion order a language guarantee?

level: juniorimportance: must knowfreq 70%

answer

  1. Once an accident, later a promise
  2. The rewrite came one release earlier
  3. Two consecutive 3.x releases, not one
  4. First insertion wins, not reassignment
  5. Equality still ignores it

basics

~20 s

Python 3.7 made dict insertion order a language guarantee. CPython 3.6 already behaved that way as an implementation detail of its rebuilt table. Iterating a dict yields keys in the order they were first inserted.

solid answer

~40 s

CPython 3.6 rebuilt `dict` around a compact table that appends entries to a dense array, so iteration came out in insertion order as a side effect — but the docs called it an implementation detail nobody should rely on. Python 3.7 promoted it to a language guarantee, so every conforming implementation must iterate a dict in first-insertion order. The precise rule is *first* insertion: reassigning an existing key's value leaves the key where it is, while deleting a key and adding it back moves it to the end. The same ordering covers `**kwargs` and class bodies. It does not touch equality — two dicts with the same pairs compare equal regardless of order, which is one reason `collections.OrderedDict` still exists. None of this changed through 3.14.

code

python · 5 lines
python
d = {"a": 1, "b": 2, "c": 3}
d["b"] = 99          # update in place: 'b' keeps its position
del d["a"]
d["a"] = 4           # re-insertion: 'a' goes to the end
print(list(d))       # ['b', 'c', 'a']

go deeper

for a junior

Know that iterating a dict gives keys in insertion order and that this is guaranteed from Python 3.7, so you do not need a special class for it. Be ready to say what happens when you overwrite an existing key's value.

for a middle

Explain the mechanics: the guarantee is about first insertion, delete-then-reinsert appends to the end, and growing the table copies entries in order. Separate the 3.6 implementation change from the 3.7 language guarantee.

for a senior

Show where the guarantee stops. Equality is order-independent, sets are not ordered at all, and relying on order across a serialization boundary needs the format to preserve it too. Say when collections.OrderedDict still earns its place.

for a principal

Own the API-contract angle: whether your own mappings promise ordering is a compatibility decision that binds you. Argue when it is right to depend on dict ordering in a wire format or config merge, and when explicit sorting is the safer contract.

### The two dates that matter There are two separate events here, and mixing them up is the single most common way this question is answered badly. **CPython 3.6** replaced the implementation of `dict` with a compact table: a sparse array of small integers acting as the hash table, pointing into a dense array of entries that is only ever appended to. Because iteration walks that dense array from front to back, keys came out in the order they were inserted. This was a *consequence* of an optimization aimed at memory, not a feature anyone designed for. The 3.6 release notes were explicit that it was an implementation detail of CPython and should not be relied upon. **Python 3.7** turned it into a promise. The language reference now states that dictionaries preserve insertion order, which binds every conforming implementation, not just CPython. From 3.7 onward, code that iterates a dict and depends on insertion order is correct Python, not a CPython trick. So the honest answer to "since when?" is: *behaved that way* since 3.6, *guaranteed* since 3.7. On 3.14 the guarantee is unchanged. ### What "insertion order" precisely means The guarantee is about **first** insertion of a key, and there are three cases worth separating. Assigning to a key that is already present updates the value in place. The key keeps its original position: ```python d = {"a": 1, "b": 2, "c": 3} d["a"] = 99 print(list(d)) # ['a', 'b', 'c'] - 'a' did not move ``` Deleting a key and inserting it again is a genuinely new insertion, so it lands at the end: ```python d = {"a": 1, "b": 2, "c": 3} del d["a"] d["a"] = 4 print(list(d)) # ['b', 'c', 'a'] ``` Growth does not disturb the order. When a dict outgrows its table, the entries are copied into the new one in their existing order, so a dict that has resized several times still iterates in first-insertion order: ```python r = {} for i in range(1000): r[f"k{i}"] = i print(list(r)[:3], list(r)[-3:]) # ['k0', 'k1', 'k2'] ['k997', 'k998', 'k999'] ``` `keys()`, `values()` and `items()` all iterate in that same order, and so does anything built on them — `list(d)`, a `for` loop, a comprehension, `json.dumps` of the mapping. ### What the guarantee does *not* cover **Equality ignores order.** `{"a": 1, "b": 2} == {"b": 2, "a": 1}` is `True`. Dicts compare as mappings: same keys, same values, order irrelevant. This surprises people who assume that because iteration is ordered, comparison must be too. If you need order-sensitive comparison, `collections.OrderedDict` compares order-sensitively against another `OrderedDict`, which is one of the few reasons it is still worth reaching for. Its other distinctive features are `move_to_end` and a `popitem` that can pop from either end. **Sets are not dicts.** `set` and `frozenset` have no ordering guarantee whatsoever, and their iteration order for `str` keys shifts between runs because of hash randomization. A candidate who says "collections are ordered now" has over-generalized. **Sorting is a different operation.** Insertion order is not sorted order. If you want keys in sorted order you still call `sorted(d)`. ### Where else ordering was fixed Two related guarantees arrived alongside this and often come up as a follow-up. Keyword arguments collected into `**kwargs` preserve the order they were written at the call site, and a class body's namespace is ordered, which is what lets declarative base classes see fields in source order. Both landed in 3.6. ### Why interviewers ask it It is a cheap probe with a surprisingly wide spread of answers. A weak candidate says dicts are unordered — knowledge frozen around Python 2 or early 3.x. A middling one says "they are ordered now" and stops. A strong one separates the 3.6 implementation change from the 3.7 language guarantee, states the first-insertion rule including the delete-then-reinsert case, and notes that equality is still order-independent. That last detail is the tell: it shows the candidate has actually reasoned about the semantics rather than absorbing a headline. ### Relying on it in practice Once the guarantee is language-level, a whole class of small conveniences becomes legitimate. Merging configuration layers into a dict and iterating the result gives a stable, reviewable order. Building a record dict and serializing it produces the same field order every run, which makes diffs of generated output readable instead of noisy. Deduplicating a sequence while keeping first-seen order collapses to one expression, because `dict.fromkeys(seq)` keeps the keys in arrival order and `list()` of it is the deduplicated sequence. Two cautions travel with that. First, the guarantee is about the mapping in memory, not about whatever you hand it to: a format, a database column order, or a receiving service may not preserve anything, so an order that matters across a boundary should be made explicit rather than inherited. Second, other interpreters are bound by the guarantee from 3.7 onward, but code that must also run on much older runtimes cannot assume it — there, ordering still needs an explicit ordered mapping.

  • Does reassigning an existing key's value move it to the end of the dict?
    No. The key already has an entry, so assignment overwrites the value in place and the key keeps its original position. Only a key that is not currently present gets appended. That is why deleting a key and adding it back does move it to the end — the delete removed the entry, so the second assignment is a fresh first insertion.
  • Do two dicts with the same pairs in different insertion orders compare equal?
    Yes. `dict.__eq__` compares them as mappings: same set of keys, and equal values for each. Order plays no part, so `{"a": 1, "b": 2} == {"b": 2, "a": 1}` is `True`. `collections.OrderedDict` is the exception — comparing two `OrderedDict` instances is order-sensitive, though comparing one against a plain dict falls back to the order-insensitive rule.
  • Is the ordering guarantee extended to sets as well?
    No. `set` and `frozenset` guarantee nothing about iteration order, and for `str` elements the order changes between interpreter runs because string hashing is randomized per process by default. If a test asserts on set iteration order it will pass locally and fail unpredictably elsewhere; sort the elements or compare sets directly instead.
  • Does `**kwargs` preserve the order the keyword arguments were written in?
    Yes, since Python 3.6. The dict a function receives as `**kwargs` lists the keywords in call-site order, and class bodies are likewise evaluated into an ordered namespace. That guarantee is what allows declarative APIs to see declared fields in source order without extra bookkeeping.

Think of a numbered guest book: the hash table tells you which line a name is on, but the book itself is written top to bottom, so reading it back always replays arrivals in order.

saying these in an interview costs you the question

  • Says Python dicts are unordered in current versions
  • Claims dict iteration returns keys in sorted order
  • Attributes the language guarantee to 3.6 rather than 3.7
  • Thinks reassigning a value moves that key to the end
  • Believes dict equality compares insertion order
  • Assumes sets became ordered at the same time

context

open as a page

How does CPython's compact dict layout preserve insertion order?

level: middleimportance: should knowfreq 45%

basics

~20 s

A CPython dict is two arrays: a sparse array of small integers that is the hash table proper, and a dense entries array appended to on insertion. Iteration walks the dense array, so order comes free.

open as a page

Why does a CPython dict still hold a 2.4 GB table after a document-conversion queue deletes every finished job?

level: seniorimportance: should knowfreq 30%

basics

~20 s

Deleting keys never shrinks a CPython dict; a delete only tombstones the slot. Tables are resized by insertions that exhaust the usable slots, so a drained dict keeps its peak-sized table until you rebuild or clear it.

open as a page

How does CPython's key-sharing dict make instance attributes cheaper than a dict?

level: seniorimportance: nice to knowfreq 15%

basics

~10 s

Almost every instance of a class carries the same attribute names, so CPython stores those names once in a keys table owned by the class and gives each instance only its own values array.

open as a page