Dict iteration order differs between Python 3.6 and 3.7+ — what should code rely on?
0 reputation · 08 Feb 2026, 12:52 UTC
0 reputation · 08 Feb 2026, 12:52 UTC
I maintain a library that iterates over dictionary keys and produces serialized output (for example, building query strings and config files) where the key order affects the result. Some deployments still run Python 3.6 while others run 3.7 or later.
My understanding is that insertion-order preservation is only an implementation detail in 3.6 (CPython-specific) and became a language guarantee from 3.7 onward. That raises a design question: can the library safely rely on plain dict ordering, or must it branch on sys.version_info and fall back to collections.OrderedDict for older interpreters?
There is also uncertainty about C extensions in the dependency tree: if a third-party extension returns mapping-like objects, do those follow the same ordering guarantee, or could they expose different semantics even on 3.7+?
Specifically:
OrderedDict fallback the only portable option?34775 reputation · 08 Feb 2026, 18:14 UTC
If your minimum supported version is 3.6, do not rely on plain dict ordering as a contract — but you also don't need a version branch. Use collections.OrderedDict (or explicitly sort keys) at the point where order affects output, and plain dict everywhere else. On 3.7+ you can rely on dict insertion order unconditionally.
The confirmed facts, separated from the surrounding folklore:
dict is part of the language specification. Every compliant implementation must honor it. Code targeting 3.7+ can rely on it fully.So the practical risk on 3.6 is not CPython itself — it is claiming a guarantee the language doesn't make, and any non-CPython 3.6 runtime your users might deploy on.
sys.version_info branch?No. Branching adds a second code path to maintain and test, and it buys nothing: OrderedDict works correctly on 3.7+ too (it is now a thin wrapper with a few extra features like move_to_end and order-sensitive equality). The portable pattern is to make ordering explicit only where it matters:
from collections import OrderedDict
def build_query(params):
# params may be any mapping; normalize at the boundary
ordered = OrderedDict(params) # or: sorted(params.items())
return "&".join(f"{k}={v}" for k, v in ordered.items())If the caller controls key order and you just need a deterministic order (e.g., config files, cache keys), sorting keys is often simpler and removes the dependency on how the mapping was constructed at all.
The 3.7+ guarantee applies to the built-in dict type, not to the abstract concept of mappings. A C extension returning a custom mapping type defines its own iteration semantics. Even on 3.7+, such an object may iterate in hash order, sorted order, or arbitrary order. Two implications:
dict (or OrderedDict) as unordered unless its documentation says otherwise.OrderedDict(mapping.items()) or sorted(mapping.items()) — so extension-provided mappings can't leak nondeterminism into your serialized output.OrderedDict or sorted keys), never by relying on raw dict iteration.assert d1 == d2 will not catch ordering bugs.[tox]
envlist = py36, py311
[testenv]
commands = pytest tests/test_serialization.pyIf you consume extension-provided mappings, add one test with a deliberately scrambled mapping (a stub object whose items() yields keys in reverse) to prove your normalization actually enforces order rather than inheriting it by luck.
Keep plain dict for internal storage, normalize to OrderedDict or sorted items at serialization boundaries, and assert on serialized bytes/strings in a two-version CI matrix. That gives you deterministic output on 3.6 and 3.7+ with a single code path and no version sniffing. If you can drop 3.6 support (it is long past end-of-life), the normalization can shrink to plain dict — but keep the boundary normalization for third-party mapping objects regardless.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.