The YAML traps that change your config without telling you
Norway becomes false, version 1.20 becomes 1.2, and a tab costs you an afternoon. Each one demonstrated in Python and Node side by side — because the two do not always agree.
YAML is the format people pick because it is readable. That readability comes from one design decision: you can write a value without saying what type it is, and the parser works it out. Most of the time that is a gift. Occasionally it decides your data means something you did not intend, and it does so without an error.
These are the ones that actually cost people time, ordered by how often I have watched them happen. Every example here was run through both PyYAML and js-yaml before publishing, because on two of these the two parsers disagree — which turns out to be a bigger problem than either behaviour on its own.
1. A tab in the indentation, and nothing else in the file matters
This is the most common YAML error by a distance, and the most annoying, because the thing that is wrong is invisible. YAML forbids tab characters in indentation outright — that is in the specification, not a quirk of any one parser.
subscriber:
msisdn: "447700900142"
plan: Unlimited 5GYAMLException: tab characters must not be used in indentationThe message is accurate and still hard to act on, because you cannot see which line it means by looking. Your editor renders a tab and two spaces almost identically. The fix is to stop generating them: set your editor to insert spaces for the Tab key, and turn on whitespace rendering for .yaml files so the difference is visible.
2. The Norway problem
The famous one. A list of ISO country codes contains NO for Norway, and the parser decides it is a boolean:
codes: [NO, SE, DK]>>> yaml.safe_load('codes: [NO, SE, DK]')['codes']
[False, 'SE', 'DK']Norway is gone and the other two are fine, which is exactly the kind of partial failure that survives a code review. The same happens to Y, N, YES, ON, OFF and their case variants — so a config of feature switches written as on and off, and a spreadsheet column of Y/N answers, are both in range.
Now run the identical file through Node:
> yaml.load('codes: [NO, SE, DK]').codes
[ 'NO', 'SE', 'DK' ]All three are strings. Nothing was coerced. So is the Norway problem fixed? Only in some of your stack.
I would argue the disagreement is worse than either behaviour on its own. A single behaviour you can learn. What you have instead is one config file that means two different things depending on which service reads it — and a Python service and a Node service sharing a deployment config will not disagree loudly. They will just quietly hold different beliefs about Norway until something downstream reconciles them.
3. Version numbers lose their trailing zero
Unquoted 1.20 is not a version string. It is a float, and floats do not carry trailing zeros:
version: 1.20
apiLevel: 1.10# PyYAML
>>> yaml.safe_load('version: 1.20')['version']
1.2
# js-yaml
> yaml.load('version: 1.20').version
1.2Your 1.20 release is now 1.2, which sorts before 1.9 and matches a tag that does not exist. And a version like 1.2.1 in the same file stays a string, because three dots is not a number — so one column ends up holding a mix of floats and strings and any comparison between them behaves oddly.
The same shape of problem eats leading zeros:
> yaml.load('msisdn: 0447700900142').msisdn
447700900142 # leading zero gone, silently4. Long identifiers get rounded, in JavaScript only
A 19-digit SIM identifier, read by the two parsers:
# js-yaml (Node)
> yaml.load('iccid: 8901240544102066246').iccid
8901240544102066000 # last three digits replaced
# PyYAML (Python)
>>> yaml.safe_load('iccid: 8901240544102066246')['iccid']
8901240544102066246 # exactPython is right because Python integers are arbitrary precision. JavaScript is wrong because every number it has is a 64-bit double, exact only to about 15 digits. This is not really a YAML bug — it is the JavaScript number problem wearing a different hat, and it applies identically to JSON.parse. There is a full write-up of that one here, including why the fix differs depending on whether you are displaying the value or re-emitting it.
For YAML specifically, quoting is the portable answer, and it is a safer habit than in JSON: iccid: "8901240544102066246" reads correctly everywhere.
5. Anchors are expanded, so the output is not the document you wrote
Anchors and merge keys let you write shared config once. They are genuinely useful, and they do not survive a conversion:
defaults: &defaults
plan: Unlimited 5G
roaming: true
london:
<<: *defaults
cellId: 4721
leeds:
<<: *defaults
cellId: 8830{
"defaults": { "plan": "Unlimited 5G", "roaming": true },
"london": { "plan": "Unlimited 5G", "roaming": true, "cellId": 4721 },
"leeds": { "plan": "Unlimited 5G", "roaming": true, "cellId": 8830 }
}The shared block has been copied into every user of it. For reading and reviewing, this is what you want — you see the values each entry actually ends up with, inherited ones included. For a round trip it is a one-way door: convert to JSON and back and your neat forty-line file is now four hundred lines with the structure flattened out of it.
6. Formatting a file throws your comments away
This one deserves to be better known, because comments are the main reason people choose YAML over JSON in the first place.
# 45s because the upstream billing API
# times out at 40s and we need to lose the race
timeout: 45timeout: 45The reason is structural, not a missing feature request. A loader builds a tree of data, and a comment is not data — it never enters the tree, so the dumper has nothing to write back. Fixing it requires a parser that keeps a concrete syntax tree of the original text, which is a different class of library.
The habit that prevents most of this
Nearly everything above is one bug with several faces: YAML guesses the type of an unquoted scalar, and it guesses using rules written for humans rather than for your data. So the rule is not "quote everything" — that would make the file worse to read, and it is the readability you came for.
The rule is narrower and easy to apply:
Two more things worth knowing while you are here. Any valid JSON is already valid YAML — YAML 1.2 is a strict superset — so when a YAML block is fighting you, dropping to JSON syntax inside it is a legitimate escape hatch. And if a document has to be structurally correct rather than merely parseable, YAML has no equivalent of an XSD; the practical answer is a JSON Schema applied to the loaded document, which is what most editors do via SchemaStore.
YAML Validator Reports the line and column of the first syntax error, and calls out tab indentation specifically rather than leaving you to decode a generic parse failure. YAML to Table Lays repeated entries out as rows, which is the fastest way to spot the one record where a value was coerced to something the others are not.Worth pinning above your desk
- Tabs are illegal in indentation. Configure the editor once and never think about it again.
NO,on,off,y,nare booleans in YAML 1.1 and strings in 1.2 — so PyYAML and js-yaml genuinely disagree. Quote them and the question disappears.1.20becomes1.2and0447…loses its zero. Version strings and phone numbers are text.- JavaScript rounds integers past ~15 digits in YAML exactly as it does in JSON. Python does not. The same file, two answers.
- Anchors expand on conversion, and comments do not survive a load-and-dump.
If YAML is not actually your requirement and you just need the data in something a parser will not argue with, YAML to JSON gets you there — and JSON vs XML covers what you gain and lose choosing between the two on the far side.