YAML Recipes
Contents
- Create a config from scratch
- Read: multi-document files and defaults
- Modify: nested keys, lists, comment-preserving edits
- Validate against a schema
- Convert YAML ↔ JSON
- Gotchas (Norway problem, implicit typing, anchors)
Create a config from scratch
python
import yaml
config = {
"server": {"host": "0.0.0.0", "port": 8080},
"features": ["auth", "metrics"],
"welcome": "Bienvenue à Montréal",
}
with open("config.yaml", "w") as f:
yaml.safe_dump(config, f,
default_flow_style=False, # block style, not {a: 1}
sort_keys=False, # keep insertion order
allow_unicode=True) # keep accented chars readableForce quotes on an ambiguous scalar:
python
yaml.safe_dump({"country": "NO", "version": "1.10"}, f,
default_style=None) # safe_dump already quotes these on output
# When hand-writing YAML text, quote them yourself: country: "NO"Read: multi-document files and defaults
python
import yaml
# Single document
data = yaml.safe_load(open("config.yaml")) or {} # empty file → None, so `or {}`
# Multi-document (--- separators), e.g. Kubernetes manifests
docs = list(yaml.safe_load_all(open("manifests.yaml")))
# Nested read with defaults
port = (data.get("server") or {}).get("port", 8080)Modify: nested keys, lists, comment-preserving edits
PyYAML (comments will be lost):
python
data = yaml.safe_load(open("config.yaml"))
data.setdefault("server", {})["port"] = 9090
data.setdefault("features", []).append("tracing")
yaml.safe_dump(data, open("config.yaml", "w"),
default_flow_style=False, sort_keys=False, allow_unicode=True)ruamel.yaml (comments, quotes, key order all survive):
python
from ruamel.yaml import YAML
y = YAML() # round-trip mode
y.indent(mapping=2, sequence=4, offset=2) # match common k8s/compose style
doc = y.load(open("docker-compose.yml"))
doc["services"]["web"]["ports"] = ["8080:80"]
y.dump(doc, open("docker-compose.yml", "w"))Multi-document write:
python
yaml.safe_dump_all(docs, open("manifests.yaml", "w"),
default_flow_style=False, sort_keys=False)Validate against a schema
For structural guarantees use jsonschema (pip install jsonschema) on the loaded data — YAML loads to the same shapes JSON Schema describes:
python
import yaml, jsonschema
schema = {
"type": "object",
"required": ["server"],
"properties": {
"server": {
"type": "object",
"required": ["port"],
"properties": {"port": {"type": "integer",
"minimum": 1, "maximum": 65535}},
}
},
}
data = yaml.safe_load(open("config.yaml"))
jsonschema.validate(data, schema) # raises ValidationError with a JSON pathConvert YAML ↔ JSON
python
import json, yaml
# YAML → JSON
json.dump(yaml.safe_load(open("config.yaml")), open("config.json", "w"),
indent=2, ensure_ascii=False)
# JSON → YAML
yaml.safe_dump(json.load(open("config.json")), open("config.yaml", "w"),
default_flow_style=False, sort_keys=False, allow_unicode=True)Caveat: JSON has no equivalent for YAML anchors, multi-doc streams, or non-string keys — conversion resolves/loses them.
Gotchas (Norway problem, implicit typing, anchors)
- The Norway problem. In YAML 1.1 (what PyYAML implements), unquoted
no,yes,on,off,y,nparse as booleans —country: NObecomescountry: False. Quote them. Same family:port: 022is octal 18,version: 1.10is the float1.1. - Timestamps auto-convert.
date: 2026-08-05loads asdatetime.date, not a string. Quote if you need the text. safe_loadof an empty file returnsNone, not{}— guard withor {}.- Duplicate keys don't error in PyYAML — the last one silently wins. ruamel.yaml raises; use it when duplicates would be a bug.
- Anchors/aliases (
&base,*base,<<:merge) load fine but PyYAML re-dumps them expanded (aliases only re-emitted when the same object identity repeats). If the user's file relies on anchors for maintainability, edit with ruamel.yaml. sort_keysdefaults to True insafe_dump— it silently alphabetizes configs; always passsort_keys=Falsewhen round-tripping.- Long strings get folded with line breaks on dump; pass
width=4096to keep long values (URLs, tokens) on one line. - YAML 1.2 vs 1.1. ruamel.yaml follows 1.2 (only
true/falseare booleans); PyYAML follows 1.1. The same file can load differently across the two libraries — pick one per task and stay with it.