YAML is everywhere — Kubernetes, Docker Compose, Ansible, GitHub Actions, CI/CD pipelines. It looks friendly until you spend an hour debugging why on became true or your port number turned into octal.

Here are the traps and how to avoid them.

The Norway Problem

1
2
3
4
5
# What you wrote
country: NO

# What YAML parsed
country: false

YAML interprets NO, no, No, OFF, off, Off as boolean false. Same with YES, yes, Yes, ON, on, On as true.

Fix: Quote strings that could be boolean:

1
2
country: "NO"
enabled: "yes"  # If you mean the string "yes"

Numbers That Aren’t

Octal Surprise

1
2
3
4
5
# What you wrote
port: 0800

# What YAML parsed
port: 512  # Interpreted as octal!

Leading zeros make numbers octal in YAML 1.1.

Fix:

1
2
port: 800      # Remove leading zero
port: "0800"   # Or quote it

Version Numbers

1
2
3
4
5
# What you wrote
version: 1.0

# What YAML parsed
version: 1  # Float, loses the .0
1
2
3
4
5
# What you wrote  
version: 3.10

# What YAML parsed
version: 3.1  # Trailing zero dropped

Fix: Always quote version numbers:

1
2
version: "1.0"
version: "3.10"

Scientific Notation

1
2
3
4
5
# What you wrote
password: 1e10

# What YAML parsed
password: 10000000000.0  # Scientific notation!

Fix:

1
password: "1e10"

Multiline Strings

Literal Block (|)

Preserves newlines exactly:

1
2
3
4
script: |
  echo "line 1"
  echo "line 2"
  echo "line 3"

Result: "echo \"line 1\"\necho \"line 2\"\necho \"line 3\"\n"

Folded Block (>)

Folds newlines into spaces:

1
2
3
4
description: >
  This is a long
  description that will
  become one line.

Result: "This is a long description that will become one line.\n"

Chomping Indicators

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Keep final newline (default)
text: |
  content

# Strip final newline
text: |-
  content

# Keep all trailing newlines
text: |+
  content

Indentation Hell

YAML uses spaces, not tabs. And indentation matters.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# Valid
parent:
  child: value

# Invalid (tabs)
parent:
	child: value  # TAB character - will fail

# Invalid (inconsistent)
parent:
  child1: value
   child2: value  # 3 spaces instead of 2

Configure your editor:

  • Spaces only, no tabs
  • Show whitespace characters
  • Consistent indent (2 spaces is common)

Empty Values

1
2
3
4
5
6
# These are all different
key1:        # null
key2: ""     # empty string
key3: null   # explicit null
key4: ~      # also null
key5: "null" # the string "null"

Anchors and Aliases

Useful but confusing:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# Define anchor
defaults: &defaults
  adapter: postgres
  host: localhost

# Use alias
development:
  <<: *defaults
  database: dev_db

production:
  <<: *defaults
  database: prod_db
  host: prod-server

Expands to:

1
2
3
4
5
6
7
8
9
development:
  adapter: postgres
  host: localhost
  database: dev_db

production:
  adapter: postgres
  host: prod-server  # Overridden
  database: prod_db

Special Characters in Keys

1
2
3
4
# These need quotes
"key:with:colons": value
"key with spaces": value
"key#with#hashes": value

Comments Gotcha

1
2
3
# This is a comment
key: value  # This is also a comment
key2: "value # but this is part of the string"

Boolean Explosion (YAML 1.1)

All of these are true:

1
2
3
4
5
6
7
8
9
- true
- True
- TRUE
- yes
- Yes
- YES
- on
- On
- ON

All of these are false:

1
2
3
4
5
6
7
8
9
- false
- False
- FALSE
- no
- No
- NO
- off
- Off
- OFF

YAML 1.2 fixed this (only true/false), but many parsers still use 1.1.

Colon in Values

1
2
3
4
5
# Broken
message: Error: something went wrong

# Fixed
message: "Error: something went wrong"

Colons followed by space start a mapping.

Timestamps

1
2
3
4
5
6
7
# These become datetime objects
date1: 2024-01-15
date2: 2024-01-15 10:30:00

# These stay strings
date3: "2024-01-15"
date4: 15-01-2024  # Not ISO format

Safe Practices

Always Quote

When in doubt, quote:

1
2
3
4
5
# Safe
version: "1.0"
country: "NO"
port: "0800"
password: "1e10"

Use a Linter

1
2
3
4
5
6
7
# yamllint
pip install yamllint
yamllint config.yml

# Or in CI
- name: Lint YAML
  run: yamllint -c .yamllint.yml .

.yamllint.yml:

1
2
3
4
5
6
extends: default
rules:
  line-length:
    max: 120
  truthy:
    check-keys: true

Validate Before Deploy

1
2
3
4
5
6
7
8
# Kubernetes
kubectl apply --dry-run=client -f config.yml

# Docker Compose
docker compose config

# Ansible
ansible-playbook --syntax-check playbook.yml

Use Schema Validation

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# JSON Schema for YAML
$schema: "https://json-schema.org/draft/2020-12/schema"
type: object
properties:
  version:
    type: string
  port:
    type: integer
required:
  - version

Debugging Tips

Parse and Dump

1
2
3
4
5
6
import yaml

# See what YAML actually parsed
with open('config.yml') as f:
    data = yaml.safe_load(f)
    print(repr(data))
1
2
# One-liner
python3 -c "import yaml; print(yaml.safe_load(open('config.yml')))"

Use yq

1
2
3
4
5
# Pretty print
yq . config.yml

# Check specific value type
yq '.port | type' config.yml

Quick Reference

GotchaExampleFix
Boolean stringsNOfalse"NO"
Octal numbers0800512800 or "0800"
Version floats1.01"1.0"
Scientific1e1010000000000"1e10"
Colonsmsg: a: b → error"a: b"
Timestamps2024-01-15 → datetime"2024-01-15"

YAML’s implicit typing is the root of most problems. When you’re not sure how something will be interpreted, quote it. A little extra typing beats hours of debugging why your Norwegian customers broke the build.