Since Python 3.7, dict preserves insertion order as a language guarantee
(it was already true as a CPython implementation detail in 3.6). That killed
the main reason OrderedDict existed, and the reflex now is to delete it on
sight.
The reflex is right about eighty percent of the time. Here is the twenty percent.
1. move_to_end
od.move_to_end(key) # to the right end
od.move_to_end(key, last=False) # to the left end
$O(1)$ and in place. A plain dict cannot do this at all. The nearest
equivalent is d[k] = d.pop(k), which moves to the right end only, and there
is no way to move to the front short of rebuilding the whole mapping.
2. popitem(last=False)
dict.popitem() pops the most recently inserted pair — LIFO. OrderedDict
takes an argument: popitem(last=False) pops the oldest, which is FIFO
eviction, in $O(1)$.
Together these two are an LRU cache, and the implementation is about ten lines:
from collections import OrderedDict
class LRU:
def __init__(self, capacity: int) -> None:
self.capacity = capacity
self._data: OrderedDict[str, int] = OrderedDict()
def get(self, key: str) -> int | None:
if key not in self._data:
return None
self._data.move_to_end(key)
return self._data[key]
def put(self, key: str, value: int) -> None:
self._data[key] = value
self._data.move_to_end(key)
if len(self._data) > self.capacity:
self._data.popitem(last=False)
This is, structurally, what functools.lru_cache does in C.
3. Order-sensitive __eq__
OrderedDict(a=1, b=2) == OrderedDict(b=2, a=1) # False
dict(a=1, b=2) == dict(b=2, a=1) # True
dict equality has always been order-insensitive and still is. If order is
part of your data’s meaning — a sequence of HTTP headers, an ordered pipeline
of transformations, a canonical serialisation — then OrderedDict gives you
an equality operator that says so, and a test asserting it will actually fail
when the order drifts.
Note the asymmetry: OrderedDict(a=1) == dict(a=1) is True. The
order-sensitive comparison only applies between two OrderedDicts.
💡You are writing a test that a config merge produced the right click to reveal
keys in the right order. Someone suggests
assert list(result.keys()) == expected_keys. Why might OrderedDict be
better, and why might it be worse?
Better: the intent lives in the type, so every comparison of that value is
order-sensitive, including ones written later by someone who did not read this
test. It also makes the invariant visible at the definition site rather than
at each assertion.
Worse: it is a viral decision. Every function that touches the value now has
to decide whether to preserve OrderedDict-ness, and a single dict(result)
or {**result} anywhere in the chain silently downgrades it — with no error,
because the annotation dict[str, int] accepts both. And OrderedDict costs
more memory than dict (it maintains a doubly-linked list alongside the
table).
For a one-off assertion, comparing list(result.items()) is clearer and
cheaper. For a value whose ordering is a load-bearing part of its contract
across a whole module, the type is the better home.
Everything else is dead weight
OrderedDict still costs more memory than dict (it carries a doubly-linked
list alongside the hash table), its repr is noisier, and **kwargs /
json.loads / comprehensions all hand you a plain dict anyway. The
typing.OrderedDict alias is deprecated in favour of subscripting
collections.OrderedDict directly.
So: use it when you need move_to_end, FIFO popitem, or order-sensitive
equality. Use dict everywhere else, and delete the ones you find that are
just there because someone learned Python before 3.7.
💡dict guarantees insertion order. Does that mean
click to reveal
sorted(d.items()) and d.items() are interchangeable if you inserted in
sorted order?
Only until someone deletes and re-inserts a key. d[k] = v on an existing key
updates the value in place and keeps the original position; but
del d[k] followed by d[k] = v moves it to the end. Those two look
equivalent in a diff and are not.
The relevant failure is a cache-invalidation or upsert path that happens to
delete before writing. Everything works, ordering silently drifts, and the
report that renders d.items() starts putting rows in a different place. If
the order matters, either sort at the point of use (cheap, explicit, immune to
upstream edits) or make it part of the type with an OrderedDict and an
order-sensitive assertion. Relying on “I inserted them in the right order” is
relying on an invariant nobody wrote down.