YAML Cheatsheet

Comments and Documents

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

Comments

# is the only comment syntax. There is no block comment form, so a commented region needs a # on every line.

# a whole-line comment
name: api      # a trailing comment
# no /* */, no <!-- -->, no ///

The rules that matter:

- A # starts a comment only at the start of a line or after whitespace. tag: a#b gives the string a#b, the hash is inert. - A # inside a quoted scalar is literal. a: '# not a comment' is the string # not a comment. - A # inside a block scalar is literal too, which is how shell scripts and Dockerfiles survive a | block. - Comments are not part of the data model. A round trip through yaml.safe_load then yaml.dump loses every comment. Use ruamel.yaml with typ="rt" if you need to edit a file and keep them.

script: |
  # this IS part of the string
  echo hello
note: 'a # b'   # -> "a # b"

Document markers

MarkerMeaning
---start of a document
...end of a document (optional)
%YAML 1.2version directive, must precede ---
%TAGtag shorthand directive

A single-document file needs no --- at all. A leading --- is conventional and harmless, and it becomes mandatory once you have directives or more than one document.

%YAML 1.2
---
name: api
...

The %YAML directive is how you ask for 1.2 semantics explicitly. Support varies: ruamel.yaml honours it, PyYAML parses it but still resolves types with its 1.1 rules, so do not rely on it to fix the Norway problem.

Multi-document streams

One file, many documents, separated by ---. This is how kubectl apply -f takes several manifests in one file.

---
apiVersion: v1
kind: Service
metadata:
  name: api
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: api

Loading a stream needs the plural API. The singular one is an error, not a silent first-document read.

LanguageSingleStream
Pythonyaml.safe_load(f)yaml.safe_load_all(f)
Go (yaml.v3)yaml.Unmarshalyaml.NewDecoder(r).Decode in a loop
RubyYAML.loadYAML.load_stream
Node (yaml)YAML.parseYAML.parseAllDocuments
CLIyq '.'yq eval-all / yq '.' file prints all
import yaml

with open("manifests.yaml") as f:
    for doc in yaml.safe_load_all(f):
        print(doc["kind"], doc["metadata"]["name"])

yaml.safe_load on that same file raises ComposerError: expected a single document in the stream. If you have ever seen that message, you have a --- you did not account for.

Empty documents and stray separators

---
a: 1
---
---
b: 2

That is three documents, and the second is null. A trailing --- at the end of a file adds an empty document too, so a: 1 followed by --- loads as [{'a': 1}, None]. Templating engines produce these constantly, so filter them:

docs = [d for d in yaml.safe_load_all(text) if d]

kubectl and helm both tolerate null documents. Your own loader probably does not, and the failure shows up as 'NoneType' object is not subscriptable.

Directives and %TAG

%TAG defines a shorthand prefix, which is how tools shorten long tag URIs. You will read these more often than you write them.

%TAG !e! tag:example.com,2024:
---
thing: !e!widget
  size: 3

!e!widget expands to tag:example.com,2024:widget. Directives apply to the next document only, so a multi-document stream repeats them after each ---.

Practical notes

- The --- must be at column zero with nothing before it on the line. An indented --- inside a block is just three dashes of content. - ... is rarely used. Its one real job is ending a document in a stream that continues with non-YAML content, and closing a document before a directive. - A BOM is allowed at the very start of a stream and is ignored there. A BOM anywhere else becomes part of a scalar. a: 1\nb: 2 parses with the key "b", which looks identical to b in every editor. - A file that begins --- | is a single document whose whole body is one literal string, which is occasionally what you want for embedding a script.