backend / python core / tricky questions / 46_dict_insertion_order.md

Dict insertion order — when did it become a guarantee?

3 min read source

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 **kwargs preserve order?” — yes, since 3.7.
  • Follow-up: “Is OrderedDict still useful?” — for move_to_end, popitem(last=False), and order-sensitive equality.
  • Follow-up: “What does popitem do 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.