2025, Nov 14 11:00

Preserving YAML key order with ruamel.yaml: iterate CommentedMap keys in insertion order and keep comments

Learn how ruamel.yaml's CommentedMap preserves insertion-ordered YAML keys for linting/auto-fix, keeping comments intact and avoiding string-only assumptions.

When you need to lint and auto-fix internal YAML while preserving inline documentation, ruamel.yaml is the go-to choice. One common requirement in such pipelines is verifying and enforcing the order of specific mapping keys. The question that naturally follows is simple: does iterating over keys from ruamel.yaml preserve insertion order, and is it safe to rely on that for checks and fixes?

Example that illustrates the question

Suppose you load a YAML document that must keep comments intact and you want to check the order of keys under settings. The YAML might look like this:

settings:
  # foo
  key2:
    prop: value
  # bar
  key1:
    prop: value
  ...

And you intend to normalize it to:

settings:
  # bar
  key1:
    prop: value
  # foo
  key2:
    prop: value
  ...

The first step is to inspect the order. A typical snippet with ruamel.yaml could be:

loader = YAML()
loader.preserve_quotes = True
doc_map = loader.load(path_ref)
sect = doc_map.get("settings", default=[])
if not isinstance(sect, CommentedMap):
    return
ordered_keys = list(sect.keys())

What actually happens under the hood

The object you get for a mapping is a CommentedMap. It is a subclass of ordereddict and that is (nowadays) a subclass of collections.OrderedDict with an added .insert() method. Its .keys() call returns a CommentedMapKeysView that holds a reference to the original mapping. Iterating over that view yields keys in the order they were inserted into the CommentedMap. In other words, building a list from .keys() preserves insertion order.

There is an important nuance about types. Although in many internal configs keys under settings might be strings, YAML itself allows any scalar or collection to act as a mapping key. That means you should not hardcode list[str] as the key type in general. The loader will faithfully round-trip entries like the following:

[2011, 10, 2]: anniversary

Solution

Rely on the fact that keys from CommentedMap are yielded in insertion order. If you need to capture them for validation or reordering logic, use .keys() and iterate or cast to list. Avoid assuming they are always strings. A minimal pattern looks like this:

yml = YAML()
yml.preserve_quotes = True
root_doc = yml.load(src_ref)
conf_section = root_doc.get("settings", default=[])
if isinstance(conf_section, CommentedMap):
    key_view = conf_section.keys()
    insertion_order = list(key_view)

If you plan to rearrange entries, you can lean on the mapping’s ordering behavior. Iteration over key_view reflects the current insertion order, which is exactly what you need to compare against a desired sequence.

Why this matters

When you are auto-fixing configuration files that must retain comments and formatting, correctness depends on predictable ordering semantics. Using CommentedMap ensures that walking the keys mirrors the actual order in the document, so checks and rewrites won’t disturb the structure you want to preserve. Being aware that keys are not guaranteed to be strings in YAML also prevents subtle type assumptions from breaking your tooling on valid but uncommon inputs.

Takeaways

If you load YAML with ruamel.yaml and receive a CommentedMap, iteration over its keys is insertion-ordered. That makes it safe to verify and enforce key order without losing comments. Just refrain from narrowing key types to strings unless you truly control the input format, because YAML allows more than simple scalar string keys.