Back to Blog
Python

Python Dict Order Preservation

python dict order preservation: Understand Python's dict order preservation guarantee, its history, practical implications, and when to use OrderedDict for compatibility.

pythondictordereddictinsertion orderpython 3.7
Illustration of a Python dictionary with numbered entries showing insertion order preservation.

Python dict order preservation became a language guarantee in Python 3.7. Before that, the CPython implementation preserved insertion order in 3.6, but it was treated as an implementation detail rather than a specification. This article explains what that guarantee means, how it affects everyday code, and where compatibility still matters.

The Guarantee and Its History

Since Python 3.7, the language specification states that dictionaries preserve insertion order. That means when you iterate over a dict, you see keys in the order they were first added. This behavior is now part of the language, not just a quirk of CPython. Python 3.6 introduced the new compact dict layout in CPython, which made order preservation a side effect. The change was adopted as a language feature in 3.7, and other implementations such as PyPy and Jython have aligned with it.

For code that must run on Python 3.5 or earlier, order preservation is not guaranteed. If you rely on it there, you may get inconsistent results across implementations and versions. That is the main reason OrderedDict still exists and remains useful for backward compatibility.

What Order Preservation Means in Practice

Consider a simple example:

config = { "host": "localhost", "port": 8080, "debug": True, } for key in config: print(key)

The output will always be host, port, debug on any Python 3.7+ interpreter. The same applies to keys(), values(), and items(). Even if you update an existing key's value, the order remains unchanged. Only when you delete a key and re-add it does it move to the end.

config["debug"] = False # order stays the same print(list(config)) # ['host', 'port', 'debug'] del config["port"] config["port"] = 9090 print(list(config)) # ['host', 'debug', 'port']

This behavior is intuitive for many developers because it matches how configuration files and JSON objects are typically read. It also makes dicts more predictable in debugging and logging output.

How Order Affects Common Operations

The order guarantee influences several dict operations beyond iteration. popitem() removes and returns the last inserted item by default, which makes dicts behave like a stack for key removal. If you need FIFO behavior, you can call popitem(last=False) to remove the first inserted item.

items = {"a": 1, "b": 2, "c": 3} print(items.popitem()) # ('c', 3) print(items.popitem(last=False)) # ('a', 1)

Equality comparisons between dicts are not affected by order. Two dicts with the same key-value pairs are equal regardless of insertion order. However, if you compare list(d1.items()) and list(d2.items()), the order matters because you are comparing lists.

d1 = {"a": 1, "b": 2} d2 = {"b": 2, "a": 1} print(d1 == d2) # True print(list(d1.items()) == list(d2.items())) # False

This distinction is important when you are serializing dicts to formats that preserve order, such as JSON. The json.dumps() function outputs keys in insertion order, so two dicts with the same content but different insertion order produce different JSON strings.

Compatibility Considerations Across Python Versions

Python 3.7 is the first version where order preservation is part of the language spec. If your project supports Python 3.6, you can rely on it in CPython but not necessarily in other implementations. For Python 3.5 and earlier, there is no guarantee at all. If you must support older versions, use OrderedDict from the collections module.

from collections import OrderedDict config = OrderedDict() config["host"] = "localhost" config["port"] = 8080

OrderedDict has been available since Python 2.7 and 3.1. It provides the same order guarantee on every version. The main difference is that OrderedDict is implemented in C in CPython and has slightly more overhead than a plain dict. It also offers methods like move_to_end() that plain dicts lack.

If you are writing a library that may be installed on older Python versions, using OrderedDict is the safest choice. For applications that already require Python 3.7+, plain dicts are usually sufficient and more concise.

Performance and Memory Tradeoffs

The compact dict layout introduced in CPython 3.6 uses less memory than the older implementation, even though it preserves order. The tradeoff is that dicts now store an extra array of indices to maintain order. This adds a small constant memory overhead per entry, but the overall memory footprint is often lower because the keys and values are stored in a dense table rather than a sparse one.

In terms of runtime performance, insertion, deletion, and lookup remain O(1) average-case. The order maintenance does not add significant cost. If you are comparing plain dicts to OrderedDict, the latter has additional overhead because it maintains a doubly linked list to support operations like move_to_end(). For most workloads, the difference is negligible, but in tight loops or memory-constrained environments, plain dicts are the better choice.

When you need to guarantee order across versions or use order-specific methods, OrderedDict is worth the small cost. When you are on Python 3.7+ and only need iteration order, a plain dict is simpler and faster.

When to Rely on Order and When Not To

Rely on dict order when you are building JSON responses, configuration objects, or any data structure where the sequence of keys matters for readability or protocol compatibility. For example, when you want a stable output for logging or API responses, insertion order gives you predictable serialization.

Do not rely on dict order when you are writing code that must run on Python versions before 3.7. Also avoid relying on order when you are using dicts as sets of keys where order is irrelevant; using a set is more appropriate. If you need order but also need to reorder keys frequently, OrderedDict provides move_to_end() and popitem(last=False) that are not available on plain dicts.

Another edge case is when you merge dicts. The | operator and {**d1, **d2} preserve the order of the left operand first, then add new keys from the right operand. This is consistent with insertion order semantics.

base = {"a": 1, "b": 2} extra = {"c": 3, "a": 4} merged = base | extra print(list(merged)) # ['a', 'b', 'c']

The order of a remains from base because it already existed; c is appended at the end. This behavior is useful when you want a deterministic merge result.

OrderedDict vs Plain Dict: A Practical Comparison

FeaturePlain Dict (Python 3.7+)OrderedDict
Order guaranteeYesYes
Backward compatibilityPython 3.7+ onlyPython 2.7+
move_to_end()NoYes
popitem(last=False)YesYes
Memory overheadLowSlightly higher
Equality with orderOrder ignoredOrder ignored

Choose OrderedDict when you need to support older Python versions or require reordering operations. Choose a plain dict for new code targeting Python 3.7+ and when you only need insertion order for iteration or serialization.

Edge Cases and Subtle Behavior

One subtle point is that dict order is based on the first insertion of a key. If you update a key's value, the position does not change. Only deletion followed by re-insertion moves the key to the end. This can surprise developers who expect an update to refresh the order.

d = {"x": 1, "y": 2} d["x"] = 10 # order remains ['x', 'y'] print(list(d)) # ['x', 'y']

Another edge case is when you create a dict from another iterable. The order is determined by the iteration order of the source. For example, dict(zip(keys, values)) preserves the order of keys. This is useful when you need to reorder a dict by creating a new one from a sorted list of keys.

original = {"b": 2, "a": 1, "c": 3} sorted_keys = sorted(original) reordered = {k: original[k] for k in sorted_keys} print(list(reordered)) # ['a', 'b', 'c']

This pattern is common when you want a canonical key order for serialization or comparison. It is also worth noting that the json module preserves order in both serialization and deserialization, so round-tripping a JSON object through Python dicts maintains the original key order.

Finally, if you are working with **kwargs in function definitions, the order of keyword arguments is preserved in the kwargs dict. This can be useful for logging or passing through options in a deterministic way.

def process(**kwargs): for key in kwargs: print(key, kwargs[key]) process(b=2, a=1) # prints b then a

Understanding these details helps you write code that behaves consistently across environments and avoids subtle bugs when order matters.

python dict order preservation: Practical Usage and Code Exa | RYUSLOG DEV