Dict insertion order — when did it become a guarantee?
The history
| Python version | dict order behavior |
|---|---|
| ≤ 3.5 | Arbitrary, depends on hash + insertion |
| 3.6 | Insertion-ordered as a CPython implementation detail (not guaranteed by spec) |
| 3.7+ | Insertion-ordered as a language guarantee — all conformant Python implementations must preserve it |
Before 3.6, iterating a dict could return keys in any order. Test suites that relied on order were brittle. Code targeting alternative interpreters (Jython, IronPython, MicroPython) couldn’t assume anything.
The 3.6 layout change (compact dict) made insertion order a free side effect — keys are stored in a dense entries array in insertion order, with a sparse hash table indexing into it. CPython promoted this to a language guarantee in 3.7 (Guido’s “I now declare it official”), once it was clear no real-world code suffered from the constraint.
What this guarantees
d = {}
d['c'] = 3
d['a'] = 1
d['b'] = 2
list(d) # ['c', 'a', 'b'] — insertion order
list(d.keys()) # ['c', 'a', 'b']
list(d.values()) # [3, 1, 2]
list(d.items()) # [('c', 3), ('a', 1), ('b', 2)]
Reassigning a value does NOT move the key:
d = {'a': 1, 'b': 2, 'c': 3}
d['a'] = 99
list(d) # ['a', 'b', 'c'] — 'a' still first
Deleting and re-inserting DOES move it to the end:
del d['a']
d['a'] = 100
list(d) # ['b', 'c', 'a'] — 'a' is now last
Knock-on guarantees
**kwargs
Function call kwargs preserve insertion order:
def f(**kwargs):
return list(kwargs)
f(a=1, b=2, c=3) # ['a', 'b', 'c']
f(c=3, a=1, b=2) # ['c', 'a', 'b']
This was historically unreliable. After 3.7, it’s a hard guarantee — important for things like SQLAlchemy filter chains, query builders, decorators that proxy through kwargs.
json.dumps
Serialized JSON preserves dict order in the output:
import json
json.dumps({"c": 3, "a": 1, "b": 2}) # '{"c": 3, "a": 1, "b": 2}'
Useful for deterministic API responses, snapshot tests, content-hashing JSON for caching.
(sort_keys=True overrides this if you want alphabetical.)
Class body / __dict__ / dataclass field order
class User:
name: str
age: int
email: str
User.__annotations__ # {'name': str, 'age': int, 'email': str} in declaration order
dataclass.fields(), __init_subclass__, __init__ parameter order — all flow from this guarantee.
Is OrderedDict ever still useful?
Yes — for three things dict doesn’t do:
1. move_to_end(key, last=True/False)
from collections import OrderedDict
od = OrderedDict([('a', 1), ('b', 2), ('c', 3)])
od.move_to_end('a') # OrderedDict([('b', 2), ('c', 3), ('a', 1)])
od.move_to_end('a', last=False) # OrderedDict([('a', 1), ('b', 2), ('c', 3)])
Useful for LRU caches: on access, move-to-end to mark “recently used.”
class LRU:
def __init__(self, max_size):
self.cache = OrderedDict()
self.max_size = max_size
def get(self, key):
if key in self.cache:
self.cache.move_to_end(key)
return self.cache[key]
return None
def set(self, key, value):
if key in self.cache:
self.cache.move_to_end(key)
self.cache[key] = value
if len(self.cache) > self.max_size:
self.cache.popitem(last=False) # evict oldest
(functools.lru_cache uses this internally.)
2. popitem(last=False) — FIFO popping
dict.popitem() always pops the last inserted (LIFO). OrderedDict.popitem(last=False) pops the first:
d = {'a': 1, 'b': 2, 'c': 3}
d.popitem() # ('c', 3) always last
od = OrderedDict([('a', 1), ('b', 2), ('c', 3)])
od.popitem(last=False) # ('a', 1) first inserted
Lets you treat an OrderedDict as a FIFO queue with O(1) operations on either end.
3. Order-sensitive equality
{'a': 1, 'b': 2} == {'b': 2, 'a': 1} # True — dict equality ignores order
OrderedDict([('a', 1), ('b', 2)]) == OrderedDict([('b', 2), ('a', 1)])
# False — OrderedDict equality compares order
Use when “the order of keys is part of the data” — e.g., serialization formats where field order matters, or comparing parsed config files.
When to still prefer plain dict
Almost always. dict is faster (no extra bookkeeping for move_to_end), more idiomatic, and order-preserving since 3.7. Only reach for OrderedDict if you specifically need move_to_end, FIFO popitem, or order-sensitive ==.
Interview angle
- Q: “Are dicts ordered in Python?” — yes, since 3.7 as a language guarantee; 3.6 as CPython detail.
- Q: “Does
**kwargspreserve order?” — yes, since 3.7. - Follow-up: “Is
OrderedDictstill useful?” — formove_to_end,popitem(last=False), and order-sensitive equality. - Follow-up: “What does
popitemdo on a regular dict?” — always pops the last inserted (LIFO).OrderedDict.popitem(last=False)pops the first.
See 02_python_core/32_dict_internals.md, 31_collection_complexity_bigO.md, stdlib/01_collections.md.