In YAML, no is false — and NO is Norway
published
TL;DR
YAML 1.1 resolves no, off, yes and on to booleans. YAML 1.2 does not. Two parsers, same file, different data:
a: no
b: NO
c: yes
d: off
PyYAML 6.0.3 (yaml.safe_load) returns {'a': False, 'b': False, 'c': True, 'd': False}. js-yaml 4.1.1 (yaml.load) returns {a: 'no', b: 'NO', c: 'yes', d: 'off'}. Quote every value you meant as a string.
The problem
Someone adds a country to a config file. Norway’s ISO 3166-1 code is NO.
country: NO
>>> import yaml
>>> yaml.safe_load('country: NO')
{'country': False}
Nothing threw. Norway is now False, and downstream you get a TypeError about a bool where a string was expected — three call frames away from the file that caused it. This is the “Norway problem”, and it is the most quoted example because the failing value looks nothing like a boolean to a human reader.
It gets worse when the coerced token is a key rather than a value. GitHub Actions workflows start with on::
>>> yaml.safe_load('on: push')
{True: 'push'}
The key is the boolean True, not the string 'on'. Any code doing cfg['on'] raises KeyError: 'on' while the data is sitting right there under a different key.
Why it happens
YAML 1.1 shipped a type repository where !!bool matches a wide set of spellings — y, yes, no, on, off, true, false and their case variants. YAML 1.2 narrowed the core schema to true and false only (plus True/TRUE-style casings). Both specs are still in active use, because the choice lives in your parser, not in your file.
| Parser | YAML version | no | on | 1.10 | 22:22 |
|---|---|---|---|---|---|
PyYAML 6.0.3 (safe_load) | 1.1 | False | True | 1.1 (float) | 1342 (int) |
js-yaml 4.1.1 (load) | 1.2 core | 'no' | 'on' | '1.10' | '22:22' |
The last two columns are the same bug wearing different clothes. 1.10 parses as the float 1.1, so a pinned version silently becomes a different version. 22:22 parses as sexagesimal — base 60 — giving 1342, which is how a MAC address or a duration turns into an integer.
One nuance worth knowing before you go quoting everything in sight: PyYAML does not coerce single-letter y and n, even though YAML 1.1 lists them.
>>> yaml.safe_load('a: y\nb: n')
{'a': 'y', 'b': 'n'}
So “YAML 1.1 parser” is not a single behaviour either. The resolver in front of you is the authority, not the spec it claims to follow.
What to do
Quote the value. This is the whole fix, and it works on every parser and every version:
country: "NO"
version: "1.10"
mac_suffix: "22:22"
Dumpers already know this. PyYAML quotes the string back out when it would otherwise re-read as a boolean:
>>> print(yaml.safe_dump({'country': 'no'}))
country: 'no'
That asymmetry is the tell: the library considers the bare form ambiguous enough to protect on write, but not on read.
Know which parser reads the file. The same repo often has several — a Python service on PyYAML, a Node build step on js-yaml, a CI runner on something else entirely. A config that round-trips fine in one can flip meaning in another. If you are migrating a codebase from js-yaml v3 to v4, that is a 1.1 → 1.2 change, and previously-boolean values become strings.
Round-trip suspicious files through JSON. JSON has exactly two boolean literals and no implicit typing, so converting shows you what your parser actually built. A false where you wrote NO is unambiguous in a way the YAML source is not — that is what the JSON ↔ YAML converter is for, and it runs in your browser, so a config with secrets in it never leaves the tab.
For new formats, prefer explicit types. If you control the schema, validate after load: assert that country is a str before anything uses it. Type coercion bugs that surface at load time cost minutes; the same bug surfacing three layers deep costs an afternoon.
Caveats
- This is not a YAML bug. Both behaviours are specified; the mismatch is between two live specification versions, and neither parser is wrong.
- Quoting is only needed for values whose bare form is ambiguous. Quoting everything is legal but makes diffs noisier, and it will not save you from a genuinely different problem like duplicate keys.
- The exact matched set varies by implementation, as the
y/ncase above shows. Test with your parser rather than reasoning from a table — including this one. - Version numbers deserve separate care:
1.10is a float in both YAML 1.1 and 1.2, because that is normal number parsing, not implicit boolean typing. Quote it either way. - Sexagesimal integers (
22:22→1342) are a YAML 1.1 feature that YAML 1.2 dropped. If you rely on the 1.2 behaviour, pin the parser version — it is what encodes the decision.