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 scalarYAML 1.1 (PyYAML 6.0)YAML 1.2 core
true True TRUEboolbool
false False FALSEboolbool
yes Yes YESbool truestring
no No NObool falsestring
on On ONbool truestring
off Off OFFbool falsestring
y Ystring in PyYAML, bool in ruamel 1.1string
n Nstring in PyYAML, bool in ruamel 1.1string

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.

WrittenPyYAML (1.1)1.2 coreWhy
06444206441.1 octal
01715171.1 octal
08"08"8invalid octal in 1.1
0o17"0o17"151.2-only octal prefix
12:30750"12:30"1.1 sexagesimal
1:00:003600string1.1 sexagesimal
190:20:30685230string1.1 sexagesimal
1.101.11.1float, the zero is gone
1e3"1e3"1000.0PyYAML needs 1.0e+3
5e+22"5e+22"5e+22PyYAML needs a decimal point
+121212signed 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: 1

fails 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
ParserBehaviour
PyYAML safe_loadsilent, last wins, {"port": 9090}
ruamel.yamlDuplicateKeyError
Go yaml.v3error
js-yamlerror with json: true, otherwise last wins
yamllintflags 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

ValueWritten plainFix
*error, alias reference'*'
*.logerror'*.log'
&xanchor with a null value'&x'
%sscan error, directive indicator'%s'
@userscan error, reserved'@user'
` cmd `scan error, reserved` 'cmd' `
!Refunknown-tag error'!Ref'
- xopens a sequence'- x'
? xcomplex-key indicator'? x'
a: bmapping-value error'a: b'
{aflow-mapping error'{a'
x #ycomment, 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.