Skip to content
← Back to explainers

Python explainer

When to Use OrderedDict Instead of dict

About 4 min to read. Allow extra time to work through examples.

A plain dict keeps insertion order. Use OrderedDict when you move keys around, remove the oldest entry, or need order to count in equality.

This article uses Python. You can make it your practice mode to browse related content across the site.

You are writing a small cache that keeps the 100 results used most recently. When it is full, it drops the one used longest ago. A plain dict keeps its keys in the order you inserted them, and d[key] = d.pop(key) moves a key to the end. But a dict has no quick way to remove the oldest entry or to move a key to the front. A collections.OrderedDict has both.

from collections import OrderedDict

values = OrderedDict(a=1, b=2, c=3)
values.move_to_end("a")
assert list(values) == ["b", "c", "a"]
values.move_to_end("c", last=False)
assert list(values) == ["c", "b", "a"]
assert values.popitem(last=False) == ("c", 3)

The move_to_end(key) method moves a key to the end, or to the front with last=False, and keeps its value. A missing key raises KeyError.

Remove from either end

dict.popitem() removes the pair you inserted last. OrderedDict.popitem(last=False) removes the first pair in the current order instead. Both raise KeyError when the dict is empty. After move_to_end, the first pair is not always the one you inserted first.

Build a small LRU cache

An LRU cache, short for least recently used, keeps a fixed number of entries and drops the one used longest ago. Move a key to the end each time you read or write it, then remove from the front when the cache is too big:

from collections import OrderedDict


class LRU:
    def __init__(self, capacity: int) -> None:
        if capacity < 0:
            raise ValueError("capacity must be 0 or more")
        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)


cache = LRU(2)
cache.put("a", 1)
cache.put("b", 2)
cache.get("a")     # "a" is now the most recent
cache.put("c", 3)  # the cache is full, so "b" goes
assert cache.get("b") is None
assert cache.get("a") == 1

Here None means a miss, which works because the stored values are ints. A capacity of 0 keeps nothing. To cache the results of a function, use functools.lru_cache instead, which a later article in this track covers.

Equality between two OrderedDicts includes order

from collections import OrderedDict

left = OrderedDict(a=1, b=2)
right = OrderedDict(b=2, a=1)
assert left != right
assert left == dict(right)
assert dict(left) == dict(right)

Two OrderedDict objects are equal only when their items are in the same order. An OrderedDict compared with a plain dict ignores order. So using OrderedDict does not make every comparison check order. To test that a result has the right items in the right order, compare list(result.items()) with the list you expect.

Check your understanding Can OrderedDict represent repeated HTTP header names? Show answer

No. It is still a dict, so each key appears once. Storing a second Set-Cookie header replaces the first one. For data that can repeat a key, use a list of (name, value) pairs, or a structure built to hold several values for each key.

Insertion order is not sorted order

mapping = {"a": 1, "b": 2}
mapping["a"] = 3
assert list(mapping) == ["a", "b"]
del mapping["a"]
mapping["a"] = 4
assert list(mapping) == ["b", "a"]
assert sorted(mapping.items()) == [("a", 4), ("b", 2)]

Changing the value of an existing key keeps its position, in a dict and in an OrderedDict. Deleting the key and adding it again puts it at the end. Neither type keeps its keys sorted. When you need sorted output, call sorted at the point where you produce that output.

Check your understanding Should a test that checks order make you switch the production code to OrderedDict? Show answer

Not by itself. Test the order directly, with list(result.items()). Use OrderedDict in the code when you need its methods or its order-aware equality. If the return type is part of your API, test the type in a separate check.

Conversions give you a plain dict

dict(ordered) and {**ordered} return a plain dict. The items stay in the same order, but the OrderedDict methods and the order-aware equality are gone. json.loads also builds plain dicts. Pass object_pairs_hook=OrderedDict if you need an OrderedDict instead.

Going further

An OrderedDict uses more memory than a dict. Choose it for one of the behaviors above, and use a plain dict when insertion order is all you need.

step 11 of 55

Everyday Standard Library Tools

Up next Generators and One-Shot Iterators