The Norway Problem
The most famous YAML bug starts with a list of country codes:
countries:
- GB
- IE
- FR
- DE
- NO # NorwayLoaded with a YAML 1.1 parser, the value of NO becomes the boolean false. Norway disappears from your dataset. The same parser will turn YES, Y, ON, OFF, TRUE, and FALSE into booleans regardless of casing. This is the origin of the "Norway problem" meme and a real bug that has shipped in production at multiple major companies.
YAML 1.2 (released 2009) restricted the boolean tokens to true and false only. The trouble is that YAML 1.1 is still the default in many parsers — including PyYAML before version 6, which means a huge fraction of the Python ecosystem inherited the bug.
The fix: quote anything that might look like a boolean. - "NO" stays a string. Or upgrade to a parser that defaults to YAML 1.2 (ruamel.yaml with explicit version, go-yaml/v3, JS yaml package).
Indentation: Two Spaces, Never Tabs
YAML uses indentation to denote nesting, exactly like Python. Unlike Python, the YAML spec explicitly forbids tabs as indentation characters. A single tab anywhere in your file is a parse error in conformant YAML 1.2 parsers.
The other indent rule is consistency within a block. Every key at the same nesting level must start at the same column. Mixing two-space and four-space indentation inside the same map produces a parse error or, worse, a subtly wrong tree:
# Wrong — image:tag is indented under "containers" but should be under "- name"
spec:
containers:
- name: app
image: nginx
image: redis # this is now a sibling of "containers", not a second container
# Right
spec:
containers:
- name: app
image: nginx
- name: cache
image: redisThe defensive habit is to configure your editor to insert spaces (never tabs) for .yml and .yaml files, and to run yamllint on save or in pre-commit. Two-space indentation is the de-facto standard; pick it once and never change.
Six Ways to Write a Multiline String
YAML supports six different multiline string syntaxes, each combining one of two folding modes (| literal, > folded) with one of three trailing-newline modes (default keep one, - strip all, + keep all). The combinations:
| Syntax | Name | Newline behavior | Trailing | Use case |
|---|---|---|---|---|
| | | Literal | Newlines preserved exactly | Single trailing newline | Shell scripts, code snippets |
| > | Folded | Newlines folded to spaces | Single trailing newline | Long prose that should re-wrap |
| |- | Literal, strip | Newlines preserved exactly | No trailing newline | Exact-byte string content |
| |+ | Literal, keep | Newlines preserved exactly | All trailing newlines kept | When trailing whitespace matters |
| >- | Folded, strip | Newlines folded to spaces | No trailing newline | Single-line value across multiple YAML lines |
| >+ | Folded, keep | Newlines folded to spaces | All trailing newlines kept | Rare; usually a mistake |
# | preserves newlines, keeps one trailing
script: |
echo hello
echo world
# Result: "echo hello\necho world\n"
# > folds newlines into spaces
description: >
This is a long
paragraph that will
re-wrap.
# Result: "This is a long paragraph that will re-wrap.\n"
# |- strips trailing newline
exact: |-
no newline at the end
# Result: "no newline at the end" (no trailing \n)In Kubernetes manifests the most common need is | for shell commands and |- for inline secret material. > is rarer in DevOps and shows up mostly in human-edited config like Hugo front matter.
Anchors and Aliases
YAML lets you define a value once with an anchor (&name) and reference it elsewhere with an alias (*name). Combined with the merge key (<<:), this gives you a primitive include/extend mechanism without external files:
defaults: &defaults
region: us-east-1
retries: 3
timeout: 30
production:
<<: *defaults
retries: 5 # override
staging:
<<: *defaults
region: us-west-2Two warnings. First, the merge key (<<:) was a YAML 1.1 extension and is not in YAML 1.2. Some modern parsers (go-yaml/v3 in strict mode) reject it. Test your target parser before relying on merge.
Second, anchors and aliases create shared mutable state if your loader produces references rather than deep copies. PyYAML loads aliases as the same Python object — mutating one mutates the other. json.dump-ing the result then crashes on the cycle. The defensive pattern is to deep-copy after load if you intend to mutate.
Implicit Type Coercion
YAML tries to be clever about types. An unquoted token that looks like a number becomes a number. One that looks like a boolean becomes a boolean. One that looks like a date becomes a date. The rules for "looks like" are sometimes surprising:
version: 1.10 # number -> 1.1 (trailing zero LOST)
api_key: 0123456789 # YAML 1.1: octal -> 342391; YAML 1.2: string
country: NO # YAML 1.1: false; YAML 1.2: "NO"
released: 2026-12-10 # date -> Date object, not the string "2026-12-10"
size: 1G # YAML: string "1G"; sometimes confused with K8s resource quantities
phone: +1-555-0100 # YAML: string; some parsers stumble on the leading +The most painful of these in practice is the leading-zero issue. A version field of 1.10 becomes the float 1.1 on parse and the trailing zero is gone forever. Postman, OpenAPI, and Helm all generate version strings; quote them aggressively to keep them strings.
Universal fix: when in doubt, quote. version: "1.10" is unambiguously a string and never coerces.
Special Characters in Strings
YAML has two flavors of quoted string. Single-quoted strings ('text') are literal — no escape sequences are interpreted, and the only escape inside is doubling the single quote (''). Double-quoted strings ("text") interpret backslash escapes like JSON: \n, \t, \uXXXX.
The unquoted (plain scalar) form is the source of every YAML surprise. A plain scalar that begins with @, `, or any of & * ! | > ' " % # is a parse error. A plain scalar containing : followed by space is parsed as a mapping. The rules for what is and is not a legal plain scalar fill several pages of the YAML 1.2 spec.
Defensive rule: if the value contains anything other than alphanumerics, underscores, dashes, dots, or spaces, quote it. Single quotes are simpler unless you actually need backslash escapes; double quotes if you need \n or Unicode escapes.
Kubernetes YAML Gotchas
Kubernetes manifests are the highest-volume YAML in production today. They surface a few extra gotchas worth calling out specifically:
- warningResource quantities are strings.
memory: 512Miis a string, not a number. Quote it if your editor is being aggressive:memory: "512Mi". - warningEnv values are always strings.
value: 8080works at apply time butkubectl editwill silently quote it. Quote from the start:value: "8080". - warningMulti-document files use
---. Three dashes alone on a line separate documents. A stray---in the middle of a manifest creates an empty second document thatkubectl applymay complain about. - warningHelm templates are YAML-shaped, not YAML.
{{ .Values.x }}renders to text and only becomes valid YAML after templating. Usehelm templateto inspect the rendered output. - warningConfigMap data values are strings.
port: 8080inconfigMap.datais invalid because data values must be strings.port: "8080"works.
Tooling That Catches the Bugs
The best defense against YAML pitfalls is making them impossible to commit. Five tools cover most cases:
| Tool | Purpose | Integration | Notes |
|---|---|---|---|
| yamllint | Style + indentation rules | pip install yamllint, pre-commit hook | Configurable rules; good defaults out of the box |
| yq | Query / transform YAML like jq | go install / brew install | Mike Farah variant; Python variant by Andrey Kislyuk also exists |
| kubeval / kubeconform | Kubernetes resource validation | CI step | kubeconform is the actively maintained fork |
| helm lint | Helm chart structural checks | helm lint ./chart | Catches templating errors before install |
| spectral | OpenAPI / AsyncAPI YAML linting | npm package | Custom rule sets for API contracts |
Minimal viable setup: yamllint in pre-commit + kubeconform in CI for any Kubernetes manifest. That alone catches indent bugs, the Norway problem, and 90% of K8s schema violations.
Validate any YAML in your browser
Paste your manifest, get parse errors and type warnings instantly — no upload required.