YAML Cheatsheet

Quoting

Use this YAML reference while you build software engineering projects, review code, or refresh the syntax you reach for most.

The decision rule

Three ways to write a string. Pick with this order, top to bottom, first match wins.

1. The value must contain \n, \t, or a \uXXXX escape → double quotes. 2. The value could be read as a number, bool, null, date, or version, or it starts with an indicator character, or it contains : or # → single quotes. 3. Otherwise → plain, no quotes.

plain: api-gateway
needs_single: '*'
needs_single_too: 'no'
needs_double: "line one\nline two"

Plain scalars, and what breaks them

A plain scalar is unquoted. It may contain most characters, but a handful of positions are reserved. These all fail or mis-parse:

InputResult
k: a: berror, "mapping values are not allowed here"
k: a:error, trailing : looks like a nested key
k: a # c"a", the # starts a comment
k: - aerror, - at the start opens a sequence
k: *aerror, * is an alias reference
k: &anull, & declared an anchor with no value
k: !aerror, !a is an unknown tag
k: ? aerror, ? is a complex-key indicator
k: {aerror, { opens flow style
k: %aerror, % is the directive indicator
` k: a ``error, backtick is reserved
k: @aerror, @ is reserved

Two cases that surprise people by working:

url: http://x.com     # ok, ':' with no space after is fine
hashtag: a#c          # ok, '#' with no space before is fine
negative: -a          # ok, '-' only opens a sequence when followed by a space
question: ?a          # ok, same rule for '?'

So the real triggers are : (colon then space), # (space then hash), and an indicator in first position. Everything else is safe plain.

Single quotes

Inside '...' there is exactly one escape: '' for a literal apostrophe. Backslashes are literal characters.

a: 'it''s fine'          # -> it's fine
b: 'C:\Users\new'        # -> C:\Users\new  (no \n here)
c: 'a #b'                # -> a #b, the hash is inert
d: '*'                   # -> *
e: 'yes'                 # -> the string "yes"

Single quotes are the right default for anything you want taken literally: Windows paths, regexes, glob patterns, Jinja and Helm templates, anything with a {{ }} in it.

Double quotes

"..." is the only form with real escapes. This is the full table.

EscapeMeaning
\0null byte
\a \bbell · backspace
\ttab
\nline feed
\v \f \rvertical tab · form feed · carriage return
\eescape (0x1B)
\"double quote
\/forward slash (for JSON compatibility)
\\backslash
\N \_next line (U+0085) · non-breaking space
\L \Pline separator · paragraph separator
\xNN8-bit code point
\uNNNN16-bit code point
\UNNNNNNNN32-bit code point
\ + newlineline continuation, joins without a space
msg: "tab\there, newline\nthere, caf\u00e9, \x41 = A"

parses to 'tab\there, newline\nthere, café, A = A', so the literal string is:

tab<TAB>here, newline
there, café, A = A

An unknown escape is a hard error, not a passthrough. "a\qb" fails to scan. This is the trap with regexes: "\d+" is invalid YAML, so write '\d+'.

Line continuation folds a long double-quoted value onto one line:

long: "the quick \
  brown fox"

parses to 'the quick brown fox', one space, because the leading indentation of the next line is stripped.

Keys follow the same rules

A key is just a scalar, so it needs quoting for the same reasons, and it hits the type resolver too.

"a b": 1          # keys with spaces work plain too, but quote for clarity
"on": true        # WITHOUT quotes, PyYAML makes this the key True
"1": one          # WITHOUT quotes this is the integer key 1
"null": x         # WITHOUT quotes this is the null key
"off": disabled   # WITHOUT quotes, PyYAML makes this the key False

on: 1 under PyYAML parses to {True: 1}, which is why GitHub Actions workflows read fine in CI (its parser is 1.2) and blow up in a local Python linter. Quote on, off, no, yes, y, n, true, false, null, and any all-digit key.

Quick reference

You wantWrite
plain wordname: api
the string "no"country: 'no'
a versionversion: '1.10'
a file modemode: '0644'
a regexpattern: '\d{4}'
a Windows pathpath: 'C:\tmp\new'
a Helm templateimage: '{{ .Values.image }}'
an embedded newlinebody: "a\nb"
a leading * or & or %glob: '*.log'
a colon-space inside textnote: 'time: now'
an empty strings: ''
a literal # mid-wordtag: a#b (no quotes needed)