YAML is an indentation-based data format used for Docker Compose files, Kubernetes manifests, CI pipelines and many other config files. This sheet is for developers who write or debug YAML by hand. The confusion it clears up: a bare word such as NO, yes or 0755 can silently become a boolean or a number, and the answer depends on whether your parser follows YAML 1.1 or 1.2. Outputs below come from PyYAML 6.0, which follows 1.1 rules, and ruamel.yaml 0.19.1, which follows 1.2.
| Construct | Syntax | Parsed as |
|---|---|---|
| Mapping | `name: web` | `{"name": "web"}` |
| Nested mapping | `env:` then an indented `PORT: 80` | Object inside object |
| Sequence | `- a` on each line | List |
| Sequence in a mapping | `tags:` then `- api` | `{"tags": ["api"]}` |
| Flow sequence | `[api, public]` | List |
| Flow mapping | `{cpu: 500m, memory: 256Mi}` | Object |
| Comment | `# text` | Ignored |
| Empty collections | `[]` and `{}` | Empty list, empty object |
| Document start and end | `---` and `...` | Separates documents in one stream |
Indentation uses spaces. The spec says tab characters must not be used for indentation, and there is no required width, but every line of one block must use the same number of spaces.
| Written as | Resolves to |
|---|---|
| `null`, `Null`, `NULL`, `~`, or an empty value | null |
| `true`, `True`, `TRUE`, `false`, `False`, `FALSE` | boolean |
| `42`, `-17`, `+3` | integer |
| `0o17` | integer 15 in octal |
| `0x1F` | integer 31 in hexadecimal |
| `3.14`, `.5`, `1e3`, `6.02e+23` | float |
| `.inf`, `-.inf`, `.nan` | float special values |
| anything else | string |
| Input | PyYAML (1.1 rules) | ruamel.yaml (1.2 rules) |
|---|---|---|
| `NO` | `False` | `'NO'` |
| `yes`, `on` | `True` | `'yes'`, `'on'` |
| `0755` | `493` | `755` |
| `1:30` | `90` | `'1:30'` |
| `1e3` | `'1e3'` (a string) | `1000.0` |
| `0o17` | `'0o17'` (a string) | `15` |
| `1.10` | `1.1` | `1.1` |
| `a: 1` repeated key | last value wins silently | `DuplicateKeyError` |
The 1.2 spec lists only true and false as booleans. YAML 1.1 also accepted y, n, yes, no, on and off in several capitalizations.
| Style | Line breaks | Final newline |
|---|---|---|
| Literal (vertical bar header) | Each one is kept | One, the default "clip" |
| Literal with `-` after the bar | Each one is kept | None, "strip" |
| Literal with `+` after the bar | Each one is kept | All trailing blank lines, "keep" |
| Folded (`>` header) | Single breaks become spaces, blank lines become newlines | One |
| Folded with `-` | Same as folded | None |
The headers are written as key: |, key: |-, key: |+, key: > and key: >-.
| Style | Escapes | Use it for |
|---|---|---|
| Plain `text` | None | Simple words and numbers |
| Single `'text'` | `''` is one quote | Text with `: ` or `#` and no backslashes |
| Double `"text"` | `\n`, `\t`, `\uXXXX`, `\"` | Text that needs escapes |
| Character | Meaning | Quote when value starts with it |
|---|---|---|
| `&name` | Anchor | Yes |
| `*name` | Alias to an anchor | Yes |
| `!tag` | Tag or type | Yes |
| `@` and backtick | Reserved | Yes |
| `: ` (colon, space) | Key separator | Quote any value containing it |
| ` #` (space, hash) | Starts a comment | Quote any value containing it |
| `-` followed by space | Sequence entry | Yes |
| `? ` | Complex key | Yes |
import json
import yaml
doc = """
name: web
replicas: 3
ratio: 0.5
enabled: true
tags: [api, public]
env:
- name: PORT
value: "8080"
- name: DEBUG
value: "false"
limits: {cpu: 500m, memory: 256Mi}
notes: ~
"""
print(json.dumps(yaml.safe_load(doc)))
{"name": "web", "replicas": 3, "ratio": 0.5, "enabled": true, "tags": ["api", "public"], "env": [{"name": "PORT", "value": "8080"}, {"name": "DEBUG", "value": "false"}], "limits": {"cpu": "500m", "memory": "256Mi"}, "notes": null}
Quote "8080" and "false" when the consumer expects a string. Unquoted, they would be a number and a boolean.
import yaml
doc = """
keep: |
line1
line2
fold: >
folded
text
new paragraph
strip: |-
no trailing newline
"""
for k, v in yaml.safe_load(doc).items():
print(k, repr(v))
keep 'line1\nline2\n'
fold 'folded text\nnew paragraph\n'
strip 'no trailing newline'
Use | for scripts and certificates, where line breaks matter. Use > for long prose. Add - when the value must not end with a newline, for example a token read from a file.
import json
import yaml
doc = """
defaults: &defaults
adapter: postgres
host: localhost
development:
<<: *defaults
database: dev
production:
<<: *defaults
host: db.example.com
database: prod
"""
print(json.dumps(yaml.safe_load(doc)))
{"defaults": {"adapter": "postgres", "host": "localhost"}, "development": {"adapter": "postgres", "host": "localhost", "database": "dev"}, "production": {"adapter": "postgres", "host": "db.example.com", "database": "prod"}}
&defaults names a node and *defaults repeats it. The << merge key copies a mapping's pairs, and keys written beside it override the copy, as host does in production. The merge key comes from the YAML 1.1 type repository and is not part of the 1.2 core schema, so check that your parser supports it.
import yaml
for d in yaml.safe_load_all("name: a\n---\nname: b\n...\n---\n- c\n"):
print(d)
{'name': 'a'}
{'name': 'b'}
['c']
A --- line starts a document and ... ends one. Use safe_load_all to read every document, because safe_load raises an error when a file holds more than one.
import yaml
tests = ['title: Note: draft', 'title: "Note: draft"', 'tag: #hash', 'tag: "#hash"', 'msg: @user', 'path: *.log', 'path: "*.log"', 'v: yes', 'v: "yes"']
for t in tests:
try:
print(t.ljust(22), yaml.safe_load(t))
except yaml.YAMLError as e:
print(t.ljust(22), 'ERROR', str(e).splitlines()[0])
title: Note: draft ERROR mapping values are not allowed here
title: "Note: draft" {'title': 'Note: draft'}
tag: #hash {'tag': None}
tag: "#hash" {'tag': '#hash'}
msg: @user ERROR while scanning for the next token
path: *.log ERROR while scanning an alias
path: "*.log" {'path': '*.log'}
v: yes {'v': True}
v: "yes" {'v': 'yes'}
Note that tag: #hash does not fail. The # starts a comment, so the value is silently empty. When in doubt, use double quotes.
from ruamel.yaml import YAML
try:
YAML(typ="safe").load("a: 1\na: 2\n")
except Exception as e:
print(type(e).__name__, str(e).splitlines()[0])
DuplicateKeyError while constructing a mapping
The spec requires mapping keys to be unique, but PyYAML's safe_load returns {'a': 2} for the same input. A duplicate key in a long file is a classic way to lose a setting without any warning, so lint YAML in CI.
country: NO loads as False in a 1.1 parser, so a list of country codes breaks at Norway. Quote it as "NO" or use a 1.2 parser.mode: 0755 becomes 493 in PyYAML but 755 in ruamel.yaml. Write "0755" for a string, or 0o755 where the parser supports 1.2 integers.version: 1.10 becomes the float 1.1 in both parsers. Quote every version number, as in "1.10". A ZIP code such as zip: 02134 is even worse, because PyYAML reads it as octal and returns 1116.a: then a tab-indented - x fails with found character '\t' that cannot start any token. Configure the editor to insert spaces for .yml and .yaml files.title: Note: draft fails with mapping values are not allowed here. Quote the value.# is a comment: tag: #hash silently yields null, and v: a #comment keeps only a. A # inside a word, as in a#b, is fine.a: keys. Use a linter or a parser that errors on duplicates.- y indented one space less than its sibling - x fails with while parsing a block mapping. Keep every item of a list at the same column.<<: *defaults works in both PyYAML and ruamel.yaml here, but it comes from YAML 1.1, so another parser may keep << as a literal key. Test your tool before relying on it.