Benchmark

Why YAML formatters delete your comments

Paste a commented Kubernetes manifest into most online YAML formatters and the comments come back gone. This is not a bug in those tools. It is a direct consequence of which function they call — and the same library will either keep every comment or destroy every comment depending on that one choice. Here are measured numbers for five libraries, and the code to reproduce them.

Results

Each library parsed the same 24-line Kubernetes Deployment containing 6 comments— two file-header lines, three standalone explanatory lines and three trailing inline notes — then serialised it straight back out. No transformation, no key changes. A perfect round-trip returns 6 of 6.

LibraryAPI usedComments kept
js-yaml 4.1.0JavaScriptload() + dump()0 / 6
yaml 2.8.1JavaScriptparse() + stringify()0 / 6
yaml 2.8.1JavaScriptparseDocument() + toString()6 / 6
PyYAML 6.0.3Pythonsafe_load() + dump()0 / 6
ruamel.yamlPythonYAML() round-trip mode6 / 6

The interesting row is the middle one. yaml version 2 appears twice, with opposite outcomes. Same library, same version, same input — parse() loses every comment and parseDocument() keeps every comment.

Why this happens

A YAML comment is not data. It is not a key, not a value, and it has nowhere to live inside a plain JavaScript object or a Python dict. So the moment a parser converts your document into native language types, the comments have already been thrown away — there was no field to put them in. Serialising that object back out cannot recover information that no longer exists.

This is why js-yaml, PyYAML, and yaml's parse() all score zero. They are not being careless; they are doing exactly what a load-into-native-types parser does. The comments are gone before dump() is ever called.

The two libraries that score 6 of 6 work differently. parseDocument()and ruamel.yaml's round-trip mode build a syntax tree rather than a plain object — a structure that models the document as written, with comments attached to the nodes they sit beside. Serialising that tree walks the original structure and re-emits the comments in place.

The cost is that you are no longer holding a plain object. Reading a value means asking the document for it rather than using ordinary property access, and that friction is exactly why so many tools reach for the simpler API and inherit the comment loss along with it.

A second finding: PyYAML reorders your keys

Unrelated to comments but worth knowing, because it surprises people in the same way. PyYAML's dump() defaults to sort_keys=True, which alphabetises every mapping:

>>> import yaml
>>> yaml.dump(yaml.safe_load("zebra: 1\napple: 2\nmango: 3\n"))
'apple: 2\nmango: 3\nzebra: 1\n'

On a Kubernetes manifest this moves apiVersion and kind away from the top and scatters related settings apart. Pass sort_keys=False to stop it.js-yaml preserves insertion order by default and needs no equivalent flag.

Reproduce it

Two files. Install js-yaml@4 and yaml@2 from npm, plusruamel.yaml from pip, then run each script against this input.

input.yaml

# Deployment for the checkout service.
# Owned by the payments team — ask before changing replicas.
apiVersion: apps/v1
kind: Deployment
metadata:
  name: checkout
  labels:
    app: checkout          # used by the Service selector
spec:
  # 3 is the minimum for the rolling update to stay available.
  replicas: 3
  template:
    spec:
      containers:
        - name: checkout
          image: registry.example.com/checkout:3.14.2
          resources:
            limits:
              # Raised from 512Mi after the Nov 3 OOMKill incident.
              memory: 1Gi
          env:
            - name: TIMEOUT_MS
              value: "2500"   # keep in sync with the gateway

JavaScript

import fs from 'fs';
import jsyaml from 'js-yaml';
import * as YAML from 'yaml';

const src = fs.readFileSync('input.yaml', 'utf8');
const count = (s) => (s.match(/#/g) || []).length;

console.log('source              ', count(src));
console.log('js-yaml load+dump   ', count(jsyaml.dump(jsyaml.load(src))));
console.log('yaml parse+stringify', count(YAML.stringify(YAML.parse(src))));
console.log('yaml parseDocument  ', count(YAML.parseDocument(src).toString()));

Python

import io, yaml as pyyaml
from ruamel.yaml import YAML

src = open('input.yaml').read()
print('source            ', src.count('#'))
print('PyYAML load+dump  ', pyyaml.dump(pyyaml.safe_load(src), sort_keys=False).count('#'))

y = YAML()
buf = io.StringIO()
y.dump(y.load(src), buf)
print('ruamel round-trip ', buf.getvalue().count('#'))

Counting # characters is a deliberately blunt measure. It is sufficient here because the test document contains no # inside any string value, so every occurrence is a real comment. On documents where that is not true, count comment nodes instead of characters.

What to do about it

  • In your own JavaScript code: use yaml v2'sparseDocument() whenever the document will be written back out. Useparse() only when you are reading values and discarding the document.
  • In your own Python code: use ruamel.yaml in round-trip mode for anything you re-serialise. Keep PyYAML for read-only loading, and passsort_keys=False if you ever dump with it.
  • Picking an online tool: paste a document with a comment in it before you trust the tool with a real manifest. It takes five seconds and tells you which of the two groups above the tool belongs to.

Our own YAML formatter uses parseDocument(), which is why it scores 6 of 6 on the test above. It also handles multi-document streams without duplicating the --- separators, and it runs entirely in your browser, so the manifest you paste is never transmitted anywhere. You can verify that claim by opening your browser's Network tab and watching no request get made.

Versions tested

Measured 19 August 2026 on Node.js 22.22.2 and Python 3.13: js-yaml 4.1.0,yaml 2.8.1, PyYAML 6.0.3, ruamel.yaml latest release. If you re-run this on newer versions and get different numbers, we would like to know — the results above are a snapshot, not a permanent property of these libraries.