Skip to content

The Famous Traps

Every line of traps.yml is valid YAML. It parses without a warning. And almost none of it means what it looks like:

country: NO
norway_also: no
switch: on
alarm: off
answer: yes

meeting_time: 1:20
ratio: 12:30:15

version: 1.20
release: 2.0

starts_on: 2026-10-17
starts_at: 2026-10-17T09:30:00

on: push

Here is the parser's side of the story — PyYAML 6.0.3, YAML 1.1 rules, via show.py:

$ python3 show.py traps.yml
country                              False                        bool
norway_also                          False                        bool
switch                               True                         bool
alarm                                False                        bool
answer                               True                         bool
meeting_time                         80                           int
ratio                                45015                        int
version                              1.2                          float
release                              2.0                          float
starts_on                            datetime.date(2026, 10, 17)  date
starts_at                            datetime.datetime(2026, 10, 17, 9, 30) datetime
True                                 'push'                       str

The Norway problem

country: NO was meant as the country code for Norway. Under YAML 1.1 rules, NO is a boolean spelling — so the value became False. A country dropdown built from parsed data quietly loses Norway (and, with ON/OFF-style codes and words elsewhere, other values besides).

It's a family of traps, not one word: yes, no, on, off, y, n — in any capitalisation — are booleans to a 1.1 parser. The whole first block of the table is that family. Under YAML 1.2 rules they're plain strings, which is why the same file can be harmless in one tool and wrong in another.

The last line is the family's most expensive member: on: push was a key — the trigger name in a CI workflow file, say. The key itself became the boolean True (see the table's last row, where the key column reads True). A program looking for the key "on" finds nothing; the workflow silently never triggers. This one has bitten a generation of CI users, because the file looks perfect.

Defence: quote anything that must be text — country: 'NO', 'on': push, answer: 'yes'. Quoting is absolute; no parser reinterprets a quoted scalar. And write booleans only as true/false, the spellings both rulebooks share.

The sexagesimal clock

meeting_time: 1:20 became the integer 80. YAML 1.1 reads 1:20 as a base-60 number — one "sixty" and twenty ones — a notation meant for times and angles. 12:30:15 is the same trick with two colons: 12×3600 + 30×60 + 15 = 45015. Anything shaped like digits:digits is at risk under 1.1 rules: times, ratios, scores, version-ish strings with a colon in them.

Defence: quote times and ratios — meeting_time: '1:20'. Under YAML 1.2 the notation is gone and the value is a string anyway (see the table below); quoting makes the file say the same thing in both.

The version that loses a digit

version: 1.20 became the float 1.2. Three characters that look like a release number are, to a parser, a decimal — and decimals don't keep trailing zeros. release: 2.0 came back as the float 2.0, not the string "2.0". Any code that compares versions as text, or prints them back, now has the wrong string.

Unlike Norway, this one is not a 1.1 quirk: a bare 1.20 is a number under YAML 1.2 too. The cross-parser table below shows both parsers agreeing on the wrong answer.

Defence: quote version numbers, always: version: '1.20'. A version is a label, not a quantity; nothing real is ever gained by letting it be a float. (2.4.1, with two dots, is safe unquoted — it's not a valid number, so parsers leave it a string. service.yml relies on that, and chapter 9's validator checks the version is still a string, in case an edit ever makes it 2.4.)

Duplicated keys: the silent overwrite

service:
  name: beacon
  retries: 2
  retries: 5

duplicate-keys.yml — the same key twice in one mapping. The specification says keys should be unique; most parsers do not check. PyYAML keeps the last value and says nothing:

$ python3 -c "import yaml; print(yaml.safe_load(open('duplicate-keys.yml')))"
{'service': {'name': 'beacon', 'retries': 5}}

The retries: 2 line might as well not exist. In a long config this is how an edit "doesn't take": you changed the first occurrence; the program reads the second. A linter catches what the parser won't — chapter 10 shows yamllint flagging exactly this file.

Same file, other rulebook

Here is the whole traps file through yq 4.47.2 (YAML 1.2), printed as JSON:

$ yq -o=json '.' traps.yml
{
  "country": "NO",
  "norway_also": "no",
  "switch": "on",
  "alarm": "off",
  "answer": "yes",
  "meeting_time": "1:20",
  "ratio": "12:30:15",
  "version": 1.2,
  "release": 2,
  "starts_on": "2026-10-17",
  "starts_at": "2026-10-17T09:30:00",
  "on": "push"
}

Line by line against the PyYAML table: every boolean trap is a string here, the clock times are strings, the dates are strings, and the key is "on" again. But version is still 1.2, and release is the number 2. The 1.2 rulebook fixed the words; it did not — could not — fix numbers, because 1.20 genuinely is how you write a number.

The lesson generalises past these two parsers: a YAML file's meaning is a joint property of the file and the parser. When a value matters, quote it, and the file means the same thing everywhere.

Two loud ones, briefly

Not all traps are silent. Two mistakes produce errors so reliably that their messages are decoded in full in chapter 11; meet them here so you recognise them.

An unquoted : inside a value — the file broken-colon.yml contains message: Warning: disk almost full:

yaml.scanner.ScannerError: mapping values are not allowed here
  in "broken-colon.yml", line 5, column 17

The parser took the second colon as structure. Quote the value (or drop the colon) and it parses.

And a tab used for indentation, in broken-tabs.yml:

yaml.scanner.ScannerError: while scanning for the next token
found character '\t' that cannot start any token
  in "broken-tabs.yml", line 6, column 1

Tabs are forbidden for indentation, in every version of the spec, no exceptions. Replace them with spaces.

Check: python3 show.py traps.yml prints False for country. If your parser prints 'NO' instead, you have a YAML 1.2 parser — and chapter 8's table tells you which of these traps you're still exposed to. (The float. The float is everyone's.)