The minimum reproducer
countries:
- NO
- SE
- DK Loaded with PyYAML's default safe_load, you get:
{'countries': [False, 'SE', 'DK']}
Norway is now False. Every downstream comparison
("NO" in countries) fails silently.
Why YAML 1.1 does this
YAML 1.1's "implicit typing" tries to infer types from unquoted scalars. The boolean rule is permissive:
y|Y|yes|Yes|YES|n|N|no|No|NO|true|True|TRUE|false|False|FALSE|on|On|ON|off|Off|OFF Any plain scalar matching that regex is a boolean. The intent was readability ("write what you mean"). The cost was a class of silent data corruption that's hard to detect without tooling.
YAML 1.2's narrower rule
YAML 1.2.2 restricts booleans to:
true | True | TRUE | false | False | FALSE Everything else is a string. Most modern parsers can opt in:
| Library | 1.2 mode? |
|---|---|
| PyYAML | No first-class 1.2 mode; use ruamel.yaml with typ="safe", version=(1, 2) |
| js-yaml v4+ | Default is 1.2 (FAILSAFE / CORE schema) |
| SnakeYAML Engine (Java) | 1.2 by default |
| go-yaml v3 | 1.2 by default |
| Symfony YAML | 1.2 since v5.4 with YAML_PARSE_NULL_AS_NULL |
| StrictYAML (Python) | Disables implicit typing entirely — treat every value as a string until you ask otherwise |
Other values that misbehave the same way
| Looks like | YAML 1.1 parses as | The pain |
|---|---|---|
NO | boolean false | Norway country code |
off | boolean false | Anything labeled "off" |
12:34:56 | integer 45296 (sexagesimal) | Time-of-day, MAC addresses |
0123 | integer 83 (octal) | Zero-padded IDs |
1e3 | float 1000.0 | Build IDs, version strings |
2024-01-01 | date object | Sometimes desired, often not |
null / ~ / empty | null | Empty configs flip to null |
The defensive rule
- Quote anything that's "really a string". Country codes, version numbers, IDs, env-var-style values. One pair of single quotes makes the whole class disappear.
- Use YAML 1.2 if your parser supports it. The default schema is much more conservative.
- Validate with a schema. If a field is supposed to be a string, make the schema require a string. A boolean where a string was expected is an immediate fail, not a silent corruption 200 lines later.
- Lint config files in CI. A linter that flags unquoted boolean-shaped values catches this before merge.
How to detect existing damage
Round-trip your config: load it, dump it back to YAML, diff. Any value that changed type during the round-trip is a candidate for quoting. For Python:
import yaml
loaded = yaml.safe_load(open("config.yml"))
print(yaml.safe_dump(loaded))
# diff against the original Reference
- YAML 1.2.2 specification
- YAML 1.1 boolean type — the original (over-)wide regex.
- "YAML: The Norway Problem" — Bram.us, popularized the name.
- StrictYAML: implicit typing removed
Related
FAQ
Why is it called the Norway problem?
A blog post from Bram.us in 2022 popularized the name. A list of country codes 'NO: ...' for Norway gets parsed by YAML 1.1 as 'false: ...' — a single bare 'NO' becomes the boolean false. The country quietly disappears from your config.
Which parsers still do this?
By default: PyYAML (Python), libyaml-based loaders, Symfony YAML, Ruby's Psych in YAML 1.1 mode, and many CI tools that wrap them. js-yaml switched to 1.2 by default in v4. Strict-YAML and yamllint flag it. Always check your loader's spec version.
Is YAML 1.2 a fix?
It restricts booleans to true / false / TRUE / FALSE / True / False. yes/no/on/off are plain strings. But adoption is uneven — many production environments are pinned to 1.1. Quoting is still the safe answer.
What's the actual fix?
Quote any string that looks like a YAML boolean. 'NO' instead of NO. Or use a loader configured for YAML 1.2, or one that disables implicit typing entirely (StrictYAML).
Are there other implicit-type traps like this?
Yes. '12:34:56' becomes a sexagesimal integer (45296) in YAML 1.1. '1e3' becomes a float (1000.0). '0123' becomes an octal integer in 1.1, a string in 1.2. Phone numbers without quotes get reformatted. The whole class of bugs goes away if you quote anything that's 'really a string'.