# 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 readable ``` Force 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 path ``` ## Convert 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`, `n` parse as booleans — `country: NO` becomes `country: False`. Quote them. Same family: `port: 022` is octal 18, `version: 1.10` is the float `1.1`. - **Timestamps auto-convert.** `date: 2026-08-05` loads as `datetime.date`, not a string. Quote if you need the text. - **`safe_load` of an empty file returns `None`**, not `{}` — guard with `or {}`. - **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_keys` defaults to True** in `safe_dump` — it silently alphabetizes configs; always pass `sort_keys=False` when round-tripping. - **Long strings get folded** with line breaks on dump; pass `width=4096` to keep long values (URLs, tokens) on one line. - **YAML 1.2 vs 1.1.** ruamel.yaml follows 1.2 (only `true`/`false` are booleans); PyYAML follows 1.1. The same file can load differently across the two libraries — pick one per task and stay with it.