
The bug in three lines
Write a list of ISO 3166 country codes the way anyone would, then load it:
$ python3 -c 'import yaml; print(yaml.safe_load("- DE\n- NO\n- SE"))' ['DE', False, 'SE']
The file is valid. The parse succeeds. Norway is now a boolean. Nothing in your pipeline complains until something downstream looks odd: a country missing from a dropdown, a lookup that throws a KeyError, or an export with the word "False" in a column of two-letter codes. We have watched this eat an afternoon of debugging in the wrong layer, because the config file looks perfectly fine when you read it back.
The cause is implicit typing. In YAML, an unquoted value has no declared type, so the parser guesses from the text. YAML 1.1 shipped a table of patterns for that guess, and no in every casing is on it.
The 22 spellings of true and false
The 1.1 boolean type on yaml.org defines exactly 22 accepted tokens:
| true | false |
|---|---|
y Y yes Yes YES | n N no No NO |
true True TRUE | false False FALSE |
on On ON | off Off OFF |
Note what’s allowed and what isn’t: lowercase, Titlecase and ALLCAPS, but nothing mixed. yES is a string. YES is a boolean.
It isn’t only Norway
Norway is the famous one because the example is so short. The same resolver table has several more entries that hit real files:
- Ontario.
ONis a boolean too, so a Canadian province list breaks the same way, as does any column of state codes. Same for a config key literally namedon, which is how GitHub Actions workflows start. Load one withyaml.safe_loadin Python and the trigger block comes back under the keyTrue, not"on". Every tool that lints Actions workflows has had to work around this. - Version numbers.
version: 1.10is a float, so it becomes1.1. The trailing zero is gone, sorting breaks, and a string comparison against "1.10" fails forever. - File modes. YAML 1.1 has octal integers, so
mode: 0777parses as the decimal 511. Under the YAML 1.2 core schema the exact same text is the number 777, because 1.2 only accepts0o777as octal. Two parsers, two numbers, no error from either. - Times. YAML 1.1 also has sexagesimal integers. PyYAML’s int pattern ends in
[-+]?[1-9][0-9_]*(:[0-5]?[0-9])+, which means1:30parses as 90 (one lot of sixty plus thirty). A duration column in a config file turns into integers you didn’t ask for. - Dates. An unquoted
2026-07-30becomes a date object under 1.1, not a string, which is a surprise the first time it reaches JSON serialisation. - Nothing. An empty value, a lone
~, andnull,NullorNULLall give you null. A key you left blank is not an empty string.
$ python3 -c 'import yaml; print(yaml.safe_load("version: 1.10\nmode: 0777\nt: 1:30"))' {'version': 1.1, 'mode': 511, 't': 90}
The commit-hash case is the one that inverts the usual advice. A short SHA like 123e456 looks like scientific notation, and under the YAML 1.2 core schema it is one, because 1.2 made the decimal point optional the way JSON does. Under YAML 1.1 it stays a string, since the 1.1 float pattern requires both a dot and a signed exponent, and there is a PyYAML issue titled exactly that ("Numbers in scientific notation without dot are parsed as string"). So on this specific trap the old, unfixed parser is the safe one and the modern spec-compliant parser is the one that hands you a float.

Fixed in 2009, still in your parser
YAML 1.2 came out in 2009 and its core schema recognises two booleans: true and false. That was seventeen years ago. The 1.1 rules are still what most code runs, and the reason is compatibility, not neglect. Millions of Ansible playbooks and CI files contain yes and no as booleans. Any library that flips to 1.2 by default breaks them all, so PyYAML and libyaml stayed on 1.1, and Ruby’s Psych went the same way.
Where the ecosystem did move, it moved unevenly. Go’s yaml.v2 resolves yes and no as booleans, yaml.v3 does not, and both are in production right now inside different tools that read the same files. Version skew in your dependency tree can therefore change the meaning of a manifest, which is a wonderful thing to discover during an incident.
This is also the strongest argument in the wider format debate. We go through it in detail in JSON vs YAML vs TOML, but the short version: the difference between the formats is not readability, it’s whether an unquoted word is allowed to mean something other than itself.
Where it bites in production
Kubernetes and Helm run on YAML, so this is not an academic curiosity.
Kubernetes converts YAML manifests to JSON before decoding them into typed Go structs. The coercion therefore happens in the YAML parser, before any schema validation runs, and the typed API is what saves you: an unquoted no in a ConfigMap value produces the familiar error "cannot unmarshal bool into Go value of type string". Ugly message, correct outcome, because it fails at apply time instead of at runtime.
Helm is the dangerous half. Values files are free-form, with no schema unless the chart author wrote a values.schema.json, so a user setting a value to no hands your templates a boolean. A template comparing that value against the string "no" never matches, the else branch runs, and you get a deployment that is wrong rather than broken. Those take days to notice.
Ansible sits at the other end: playbooks are full of gather_facts: no, which works only because YAML 1.1 coerces it. That is why ansible-lint carries a yaml[truthy] rule pushing everyone toward true and false, and why mixed codebases end up with both styles.
Fixes that hold
countries: - DE - NO - SE version: 1.10 mode: 0777
countries: - "DE" - "NO" - "SE" version: "1.10" mode: "0777"
- Quote anything that could be misread.
country: "NO"is immune to every parser and every spec version. Quote by category, not by inspection. Country codes, version numbers, commit hashes, times, phone numbers, zip codes, anything from user input. Reviewing each value individually is how the one bad line slips through. - Add a linter with the truthy rule on. yamllint’s
truthyrule allows onlytrueandfalseby default and flags everyyes,no,onandoffin the file. It runs in CI in about a second and it catches the class of bug, not the instance. - Understand what schema validation can and cannot do. JSON Schema, values.schema.json and Kubernetes’ typed API all run after parsing. They can tell you a value arrived as a boolean when a string was expected, which is worth a lot. They cannot give you back the text you wrote, because by then the characters N and O no longer exist anywhere in memory.
- For config you own, consider StrictYAML. Colm O’Connor built it around this exact complaint: every value is a string unless a schema says otherwise, and the features behind YAML’s other bugs (anchors, aliases, flow style) are gone. You pay with a schema you have to declare up front. For an application’s own config that is a fine trade, and it’s no use at all for files a platform parses for you.
What does not work: telling your team to remember. This bug has been rediscovered continuously since 2005 by people who already knew about it.
Seeing what your YAML really is
The fastest way to find out what a file actually parses to is to convert it to JSON and read the types, because JSON has no implicit typing to hide behind. A string carries quotes, a boolean doesn’t, and there is no third possibility. Paste the file into our YAML to JSON converter and look for bare false where you wrote NO, for 1.1 where you wrote 1.10, and for 511 where you wrote 0777. Every one of those is a line that needs quotes.
Going the other way, our JSON to YAML converter writes output using the YAML 1.1 schema deliberately: strings like NO, 1.10, ON and 2026-07-30 come out quoted automatically, so the file you generate keeps its types in the 1.1-era parsers most projects still run. A converter that emitted them bare would produce a file that is technically valid and quietly wrong in PyYAML.
Both tools run entirely in the browser with no upload, which matters more for config than for most data. The YAML files people need to check are usually the ones holding hostnames, connection strings and tokens. If you would rather stay in the terminal, the local equivalent in Python is one line: load the file with yaml.safe_load and print the repr of the parsed object, then look at what has quotes around it. Same answer, more typing.
What people ask about YAML booleans
What is the Norway problem in YAML?
The Norway problem is what happens when a YAML parser reads the country code NO as the boolean false. A list of ISO country codes written as DE, NO, SE loads as "DE", false, "SE", with no error and no warning, because YAML 1.1 treats no, yes, on and off as boolean spellings. The name comes from StrictYAML’s documentation, where Colm O’Connor uses it as the headline example of why implicit typing is a bad idea.
Which values does YAML treat as booleans?
The YAML 1.1 boolean type lists 22 spellings: y, Y, yes, Yes, YES, n, N, no, No, NO, true, True, TRUE, false, False, FALSE, on, On, ON, off, Off and OFF. YAML 1.2 cut that down to true and false only, but most parsers still run the 1.1 set. PyYAML is a partial exception worth knowing about: its resolver matches 18 of the 22 and leaves a bare y or n as a string.
Does YAML 1.2 fix the Norway problem?
On paper, yes: YAML 1.2 was published in 2009 and its core schema recognises only true and false as booleans. In practice it depends entirely on the library, not the spec. PyYAML still ships the 1.1 rules, and so do libyaml and Ruby’s Psych, so yaml.safe_load in Python turns NO into False today. Go’s yaml.v3 did switch to the 1.2 behaviour, which means the same file can parse differently in two Go tools depending on which version they vendored.
How do I stop YAML from converting my strings?
Quote them. A value in single or double quotes is never coerced, so country: "NO" stays the string NO in every parser and every spec version. For values that come from user input, IDs, version numbers, country codes and anything hex-ish, quote by default rather than case by case. The explicit tag form country: !!str NO works too, but quoting reads better and survives a copy-paste into another file.
Why does my version number 1.10 become 1.1 in YAML?
Because an unquoted 1.10 matches YAML’s float pattern, so it is parsed as the number 1.1 and the trailing zero is gone. This is the same class of bug as the Norway problem and it is more common in the wild, since version numbers land in CI configs constantly. Anything that looks like a number and needs to keep its exact text (versions, phone numbers, zip codes, part numbers) has to be quoted.
Does the Norway problem affect Kubernetes and Helm?
Yes, and it shows up differently in each. Kubernetes converts YAML manifests to JSON before decoding them, so an unquoted no in a string field usually produces a hard error like "cannot unmarshal bool into Go value of type string", which at least fails loudly. Helm values files are untyped, so the coercion is silent: a value written as no arrives in your templates as a boolean, and a comparison against the string "no" simply never matches.
Is JSON affected by the Norway problem?
No. JSON has no implicit typing at all: a string is whatever sits between double quotes, and NO can only ever be the two characters N and O. That is why converting a YAML file to JSON is a useful diagnostic. If a value you wrote as NO comes out as the JSON literal false instead of the string "NO", the coercion already happened and you know exactly which line to quote.
What is StrictYAML and should I use it?
StrictYAML is a Python YAML parser by Colm O’Connor that deliberately ignores parts of the spec, including implicit typing: every value is a string until a schema says otherwise. It also drops anchors, aliases and flow style, the features behind YAML’s security bugs. It is a good fit for application config you control, and no help at all for Kubernetes or GitHub Actions files, where the parser is chosen by the platform and quoting is the only lever you have.