Skip to content
← All articles

OrderedDict in 2026: the three things dict still cannot do

Reviewers see OrderedDict and assume Python 2. Three of its behaviours genuinely survived the 3.7 ordering guarantee — knowing which lets you defend it or delete it.

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.