YAML Cheatsheet
Gotchas
Use this YAML reference while you build software engineering projects, review code, or refresh the syntax you reach for most.
The Norway problem
NO is the ISO country code for Norway. Under a YAML 1.1 parser it is the boolean
false.
countries: - GB - NO - FR
parses to ['GB', False, 'FR'] under PyYAML. The full set, verified:
| Plain scalar | YAML 1.1 (PyYAML 6.0) | YAML 1.2 core |
|---|---|---|
true True TRUE | bool | bool |
false False FALSE | bool | bool |
yes Yes YES | bool true | string |
no No NO | bool false | string |
on On ON | bool true | string |
off Off OFF | bool false | string |
y Y | string in PyYAML, bool in ruamel 1.1 | string |
n N | string in PyYAML, bool in ruamel 1.1 | string |
The y/n row is the subtle one. The YAML 1.1 spec lists y|Y|n|N as booleans,
PyYAML's resolver omits them, and ruamel.yaml in 1.1 mode includes them. So
y: 1 is {'y': 1} under PyYAML and {True: 1} under ruamel 1.1.
Keys are affected too. on: push under PyYAML gives {True: 'push'}, which is why
a GitHub Actions workflow reads correctly in CI (its parser follows 1.2) and breaks
in a local Python script. Quote on, off, yes, no, y, n.
Numbers that are not what they look like
Every result below was parsed, not remembered.
| Written | PyYAML (1.1) | 1.2 core | Why |
|---|---|---|---|
0644 | 420 | 644 | 1.1 octal |
017 | 15 | 17 | 1.1 octal |
08 | "08" | 8 | invalid octal in 1.1 |
0o17 | "0o17" | 15 | 1.2-only octal prefix |
12:30 | 750 | "12:30" | 1.1 sexagesimal |
1:00:00 | 3600 | string | 1.1 sexagesimal |
190:20:30 | 685230 | string | 1.1 sexagesimal |
1.10 | 1.1 | 1.1 | float, the zero is gone |
1e3 | "1e3" | 1000.0 | PyYAML needs 1.0e+3 |
5e+22 | "5e+22" | 5e+22 | PyYAML needs a decimal point |
+12 | 12 | 12 | signed int |
mode: 0644 becoming 420 is the classic. So is version: 1.10 becoming the
float 1.1, which then compares as less than 1.9. Quote every version and
every file mode.
Tabs
A tab in the indentation is a hard syntax error. There is no lenient mode.
a:
b: 1fails with found character '\t' that cannot start any token. PyYAML rejects a
tab immediately after a : too. Tabs inside a quoted scalar or inside a block
scalar's content are fine. Set your editor to expand tabs in .yml/.yaml.
Duplicate keys
port: 8080 port: 9090
| Parser | Behaviour |
|---|---|
PyYAML safe_load | silent, last wins, {"port": 9090} |
ruamel.yaml | DuplicateKeyError |
Go yaml.v3 | error |
js-yaml | error with json: true, otherwise last wins |
yamllint | flags it as key-duplicates |
In a 400-line manifest a duplicated key silently erases the first value. Run
yamllint in CI rather than hoping the parser complains.
Whitespace and encoding
- Trailing whitespace in a > folded block survives, and the fold space is
added after it. a folded with b gives "a b", four spaces. Strip
trailing whitespace on save.
- Trailing whitespace in a | literal block survives too. "a \nb\n". This
breaks a shell script embedded in a | block only rarely, but it breaks a diff
every time.
- CRLF is handled. PyYAML normalises \r\n to \n, inside block scalars too.
A file with mixed line endings still parses.
- A BOM is legal only at the very start of a stream. Anywhere else it becomes
part of a scalar: a: 1\n<BOM>b: 2 gives the key "b", which renders
identically to b in every editor and fails every lookup.
- No trailing newline at end of file is fine.
Null versus empty string
a: b: "" c: null d: "null"
gives {'a': None, 'b': '', 'c': None, 'd': 'null'}. A key with nothing after the
colon is null, not an empty string and not an empty map. args: is None,
which is why a program iterating args crashes with
'NoneType' object is not iterable. Write args: [] or env: {} when you mean
empty.
Indicators that need quotes
| Value | Written plain | Fix |
|---|---|---|
* | error, alias reference | '*' |
*.log | error | '*.log' |
&x | anchor with a null value | '&x' |
%s | scan error, directive indicator | '%s' |
@user | scan error, reserved | '@user' |
` cmd ` | scan error, reserved | ` 'cmd' ` |
!Ref | unknown-tag error | '!Ref' |
- x | opens a sequence | '- x' |
? x | complex-key indicator | '? x' |
a: b | mapping-value error | 'a: b' |
{a | flow-mapping error | '{a' |
x #y | comment, becomes "x" | 'x #y' |
-a, ?a, a#b, and http://x.com are all fine plain. The trigger is an
indicator in first position, : anywhere, or # anywhere.
Miscellaneous traps
- > folding is wrong for anything line-oriented. A PEM certificate, a shell
script, or a CSV in a > block arrives as one line. Use |.
- An alias is a reference, not a copy. dev: *defaults and prod: *defaults
point at the same object. Mutating one mutates both.
- << is shallow. Overriding a nested mapping replaces it entirely.
- Anchors are per document. They do not cross a ---.
- yaml.safe_load on a multi-document file raises
expected a single document in the stream. Use safe_load_all.
- Never call yaml.load without a Loader. safe_load is the only correct
default for input you did not write.
- A comment round trip loses comments. safe_load then dump strips them all.
Use ruamel.yaml with typ="rt" to edit a file in place.
- Deeply indented files hit parser limits in some tools long before the spec's,
and a self-referencing anchor is a denial-of-service vector.