Data Formats (JSON, YAML, TOML, XML, CSV) Module 5 – YAML 1.2 and its traps
YAML type traps: Norway, octal, sexagesimal
Why YAML can turn NO into false, 0755 into 493 and 22:22 into a number: the YAML 1.1 rules, what Psych really did, probe files and the defences that work.
What you will learn
- Predict how YAML 1.1 rules, Psych and the core schema read NO, 0755, 22:22, 3.10 and 1e3
- Recognise keys, dates and nulls that a parser coerces as it coerces values
- Identify which rules a tool's parser follows, with a probe file
- Prevent coercion with quotes and a type check after loading, and choose a defence for your files
Before you start
What you’ll learn · Before you start
What you will learn
- Predict how YAML 1.1 rules, Psych and the core schema read NO, 0755, 22:22, 3.10 and 1e3
- Recognise keys, dates and nulls that a parser coerces as it coerces values
- Identify which rules a tool's parser follows, with a probe file
- Prevent coercion with quotes and a type check after loading, and choose a defence for your files
Before you start
On this page
A branch of the bookshop ships to six countries, written as two-letter codes in a YAML list. Read by Psych, the YAML
parser in Ruby’s standard library, the list comes back with five strings and one false: NO, the code for Norway, is
a boolean under the YAML 1.1 rules. In the same way 0755 became 493, 22:22 became 80520 and the version 3.10
became 3.1, and no parser reported an error, because to a YAML 1.1 parser each of them is a valid boolean or number.
You will put each trap into a table of three readings, the YAML 1.1 type pages, what Psych 5.3.1 really did and the YAML 1.2 core schema; see that keys, dates and nulls are coerced like values; ask a tool which rules it follows with a probe file; and choose the defence that fits your files.
Booleans: why NO comes back as false
Here is the list, and below it the same codes in quotes:
# Countries the Pune branch ships books to, as ISO 3166 two-letter codes
ships_to: [IN, NP, LK, NO, SE, DK]
quoted: ["IN", "NP", "LK", "NO", "SE", "DK"] The YAML 1.1 type page for booleans lists 22 words: y, yes, true, on and their opposites n, no, false,
off, each in up to three capitals. NO is one of them, so a parser that follows those rules makes Norway false.
This is the Norway problem: The YAML 1.1 trap in which the plain scalar NO, the two-letter code for Norway, loads as the boolean false, because no, NO, yes, on and off are boolean words under the YAML 1.1 rules.. The program reads the list, then puts eleven words
through the type page’s rules, through Psych and through the core schema:
# The Norway problem: the countries a branch ships to, and the words YAML 1.1 calls booleans.
require "yaml"
require "date"
require_relative "rules"
ships_to = YAML.safe_load(File.read("countries.yaml"))
puts "Psych read the list as: #{ships_to['ships_to'].inspect}"
puts "and the quoted one as: #{ships_to['quoted'].inspect}"
puts
puts format("%-6s %-16s %-16s %s", "plain", "YAML 1.1 pages", "Psych 5.3.1", "YAML 1.2 core")
%w[NO no No nO yes on off y Y n True].each do |text|
puts format("%-6s %-16s %-16s %s", text, read_with(YAML_11, text), read_with_psych(text), read_with(CORE, text))
end Output
Psych read the list as: ["IN", "NP", "LK", false, "SE", "DK"] and the quoted one as: ["IN", "NP", "LK", "NO", "SE", "DK"] plain YAML 1.1 pages Psych 5.3.1 YAML 1.2 core NO bool false bool false str no bool false bool false str No bool false bool false str nO str bool false str yes bool true bool true str on bool true bool true str off bool false bool false str y bool true str str Y bool true str str n bool false str str True bool true bool true bool true
Recorded with ruby 4.0.7 (revision 229531a6cf) +PRISM [arm64-darwin23] on macOS 26 arm64. To run it yourself: ruby booleans.rb
The list came back as ["IN", "NP", "LK", false, "SE", "DK"], while the quoted list kept all six codes: quotes make a
string under every rulebook. The table shows that Psych does not follow the type page to the letter. It reads yes,
no, on, off, true and false in any mix of capitals, so nO is false too, yet it leaves the one-letter y,
Y and n as strings, although the page lists them. Its source shows both choices, a case-insensitive pattern for the
six words and no rule for single letters.
The core schema of YAML 1.2 ends the guessing. The specification changes page says that only true and false “are
parsed as booleans”, in three capitalisations, and that yes, on and the rest are strings. That is the right column:
everything is a string except True.
Codes, flags and answers left plain
Wrong: ships_to: [IN, NO], gift_wrap: Y, answer: no. Each one is a string or a boolean depending on the parser.
Right: quote every value that is a code or a word, ["IN", "NO"], "Y", "no", and write real booleans as true
and false, the only spellings that every rulebook reads the same way.
Checkpoint
Check: Boolean words and the Norway problem
Numbers: octal, base 60 and a version that lost its zero
Numbers have more traps than words, because YAML 1.1 has more ways to write a number. This file holds the two rulebooks as patterns, the YAML 1.1 type pages and the core schema of YAML 1.2.2, section 10.3.2:
# Two rulebooks for plain scalars, written out as patterns: the YAML 1.1 type pages
# (yaml.org/type: bool, int, float, null, timestamp) and the YAML 1.2.2 core schema (§10.3.2).
# Each returns a short description such as "int 493", "bool false" or "str".
YAML_11 = [
["null", /\A(?:~|null|Null|NULL|)\z/],
["bool true", /\A(?:y|Y|yes|Yes|YES|true|True|TRUE|on|On|ON)\z/],
["bool false", /\A(?:n|N|no|No|NO|false|False|FALSE|off|Off|OFF)\z/],
["int 2", /\A[-+]?0b[0-1_]+\z/],
["int 8", /\A[-+]?0[0-7_]+\z/],
["int 10", /\A[-+]?(?:0|[1-9][0-9_]*)\z/],
["int 16", /\A[-+]?0x[0-9a-fA-F_]+\z/],
["int 60", /\A[-+]?[1-9][0-9_]*(?::[0-5]?[0-9])+\z/],
# The float page writes [0-9.]* after the dot, which would also match 1.4.2; one dot is read here.
["float", /\A[-+]?(?:[0-9][0-9_]*)?\.[0-9_]*(?:[eE][-+][0-9]+)?\z/],
["timestamp", /\A[0-9]{4}-[0-9]{2}-[0-9]{2}\z/],
].freeze
CORE = [
["null", /\A(?:null|Null|NULL|~|)\z/],
["bool true", /\A(?:true|True|TRUE)\z/],
["bool false", /\A(?:false|False|FALSE)\z/],
["int 10", /\A[-+]?[0-9]+\z/],
["int 8", /\A0o[0-7]+\z/],
["int 16", /\A0x[0-9a-fA-F]+\z/],
["float", /\A[-+]?(?:\.[0-9]+|[0-9]+(?:\.[0-9]*)?)(?:[eE][-+]?[0-9]+)?\z/],
].freeze
def read_with(rules, text)
rule, = rules.find { |_, pattern| pattern.match?(text) }
return "str" unless rule
digits = text.delete("_")
case rule
when "int 2" then "int #{Integer(digits.sub('0b', ''), 2)}"
when "int 8" then "int #{digits.sub('0o', '').to_i(8)}"
when "int 10" then "int #{digits.to_i}"
when "int 16" then "int #{Integer(digits.sub('0x', ''), 16)}"
when "int 60" then "int #{digits.split(':').map(&:to_i).reduce { |sum, part| sum * 60 + part }}"
when "float" then "float #{Float(digits)}"
else rule
end
end
# What Psych 5.3.1 builds, in the same short form.
def read_with_psych(text)
value = YAML.safe_load("v: #{text}", permitted_classes: [Date])["v"]
case value
when true, false then "bool #{value}"
when nil then "null"
when Integer then "int #{value}"
when Float then "float #{value}"
when Date then "Date"
else "str"
end
end The next program uses it to read fourteen plain scalars, next to what Psych builds from each:
# Numbers that are not what they look like, under the three rulebooks.
require "yaml"
require "date"
require_relative "rules"
SCALARS = %w[010 0755 0o755 08 0b101 1_000 22:22 1:30 190:20:30 3.10 1e3 1.5e+3 1,000 1,00,000]
puts format("%-10s %-16s %-16s %s", "plain", "YAML 1.1 pages", "Psych 5.3.1", "YAML 1.2 core")
SCALARS.each do |text|
puts format("%-10s %-16s %-16s %s", text, read_with(YAML_11, text), read_with_psych(text), read_with(CORE, text))
end
# Psych's option for integers as the YAML 1.1 page writes them: no commas.
puts
puts "1,00,000 with strict_integer: #{YAML.safe_load('amount: 1,00,000', strict_integer: true)['amount'].inspect}" Output
plain YAML 1.1 pages Psych 5.3.1 YAML 1.2 core 010 int 8 int 8 int 10 0755 int 493 int 493 int 755 0o755 str str int 493 08 str str int 8 0b101 int 5 int 5 str 1_000 int 1000 int 1000 str 22:22 int 1342 int 80520 str 1:30 int 90 int 5400 str 190:20:30 int 685230 int 685230 str 3.10 float 3.1 float 3.1 float 3.1 1e3 str str float 1000.0 1.5e+3 float 1500.0 float 1500.0 float 1500.0 1,000 str int 1000 str 1,00,000 str int 100000 str 1,00,000 with strict_integer: "1,00,000"
Recorded with ruby 4.0.7 (revision 229531a6cf) +PRISM [arm64-darwin23] on macOS 26 arm64. To run it yourself: ruby numbers.rb
Read the rows in groups:
- Octal. In YAML 1.1 a leading
0means base 8, so010is 8 and the file mode0755is 493. The core schema reads them as 10 and 755 and wants0o755for octal, which YAML 1.1 keeps as a string.08shows the edge: 8 is no octal digit, so YAML 1.1 leaves it as text, while the core schema reads 8. - Base 60. The YAML 1.1 integer page allows base-60 integer: A YAML 1.1 integer written in parts separated by colons, each worth 60 times the next, such as 1:30 for 90. YAML 1.2 dropped the form, so 1:30 is a string there., “convenient for time
and angle values”, so
1:30is 90 and190:20:30is 685230. Psych goes further: it reads two parts as hours and minutes, so1:30became 5400 and22:22became 80520 rather than the 1342 of the type page. Docker’s Compose reference says a port mappingHOST:CONTAINER“should always be specified as a (quoted) string” for this reason. - Binary and underscores.
0b101and1_000are numbers only in YAML 1.1; the changes page says YAML 1.2 dropped binary and underscores. - Floats.
3.10is the float 3.1 under every rulebook, so a version number loses its zero everywhere.1e3is the opposite case: YAML 1.1 floats need a dot and a signed exponent, so it stays text there, as in the JSON numbers of the first lesson of this module, while the core schema reads 1000.0. - Commas.
1,000matches no rulebook, yet Psych read 1000: its scanner allows commas in integers, which a comment in its source calls “Not to YML spec, but kept for backwards compatibility”. The last line shows the option that turns this off,strict_integer: true.
Amounts in lakh grouping
Indian digit grouping sets off the last three digits and then every two digits before them, as in 1,00,000 for
one lakh. Psych read it as the integer 100000, without an error, while the YAML 1.1 type pages and the core schema
both keep it as a string. Write such amounts in quotes, or as plain digits without grouping, such as 100000.
Every row of the table is a case of the same rule:
Key idea
A value that only looks like a number is a number to some parser: quote versions, codes, file modes, times and grouped amounts, and leave plain only the numbers you mean as numbers.
Checkpoint
Check: Octal, base 60, floats and commas
Keys, dates and nulls are coerced too
A key of a mapping is a plain scalar like any value, so the same rules give it a type. The first lines of a CI
workflow file show why that matters: GitHub’s workflow syntax uses a key named on “to define which events can cause
the workflow to run”.
# A CI workflow: its trigger key is the word on
name: tests
on: [push]
jobs:
test:
runs-on: ubuntu-latest The program reads the workflow, a mapping keyed by country codes, a date and two nulls:
# Keys are plain scalars too: the same rules decide their type.
require "yaml"
require "date"
workflow = YAML.safe_load(File.read("workflow.yaml"))
puts "Keys of workflow.yaml: #{workflow.keys.inspect}"
puts "workflow['on']: #{workflow['on'].inspect}, workflow[true]: #{workflow[true].inspect}"
puts
capitals = YAML.safe_load("IN: New Delhi\nNO: Oslo\nSE: Stockholm\n")
puts "Capitals by code: #{capitals.inspect}"
puts
# Dates and nulls: values that become an object, or nothing at all.
day = YAML.safe_load("opened: 2026-10-04", permitted_classes: [Date])["opened"]
puts "opened: #{day.class} #{day}"
puts "middle_name: ~ gives #{YAML.safe_load('middle_name: ~')['middle_name'].inspect}"
puts "note: null gives #{YAML.safe_load('note: null')['note'].inspect}" Output
Keys of workflow.yaml: ["name", true, "jobs"]
workflow['on']: nil, workflow[true]: ["push"]
Capitals by code: {"IN" => "New Delhi", false => "Oslo", "SE" => "Stockholm"}
opened: Date 2026-10-04
middle_name: ~ gives nil
note: null gives nil
Recorded with ruby 4.0.7 (revision 229531a6cf) +PRISM [arm64-darwin23] on macOS 26 arm64. To run it yourself: ruby keys.rb
The workflow’s keys came back as "name", true and "jobs": the trigger key became the boolean true, so a program
that looks up workflow["on"] gets nil and finds no trigger at all. The capitals have the same flaw: Oslo is filed
under false. A key changes the shape of the data, not only one value, which makes these bugs hard to trace.
Dates are the YAML 1.1 timestamp type. Psych built a Date for opened, because the program allowed Date by name;
without that, safe_load refuses the document, as the tags lesson showed. The core schema has no timestamp type and
keeps the text. Both nulls, ~ and null, became nil in Psych, and they are null in the core schema too, so a field
that is meant to hold the text null or ~ needs quotes as well.
Quote keys that are words or codes
Write "on": [push] and "NO": Oslo when a file may be read with YAML 1.1 rules. The tool a file is written for may
read it correctly, but every other program that loads the same file follows its own parser’s rules.
Checkpoint
Check: Coerced keys, dates and nulls
Ask your tool which rules it follows
A library’s name does not tell you its rules: some follow YAML 1.1, some YAML 1.2, some mix them, and some can be switched. The quickest answer is a A small YAML file whose values are read differently under YAML 1.1 and YAML 1.2 rules, loaded by a tool to show which rules its parser follows.: a handful of values that the two rulebooks read differently, loaded by the tool you want to know about:
# probe.yaml: one value for each rule that YAML 1.1 and YAML 1.2 read differently
word: no
octal: 010
new_octal: 0o10
base_60: 1:30
float: 1e3
underscore: 1_000 This program loads the probe with Psych and says, value by value, which rulebook the reading matches:
# Ask a parser which rules it follows: load probe.yaml and compare with both rulebooks.
require "yaml"
EXPECTED = { # key => [YAML 1.1 reading, YAML 1.2 core reading]
"word" => [false, "no"], "octal" => [8, 10], "new_octal" => ["0o10", 8],
"base_60" => [90, "1:30"], "float" => ["1e3", 1000.0], "underscore" => [1000, "1_000"],
}
YAML.safe_load(File.read("probe.yaml")).each do |key, value|
y11, y12 = EXPECTED[key]
follows = if value == y11 && value.class == y11.class then "YAML 1.1"
elsif value == y12 && value.class == y12.class then "YAML 1.2"
else "neither"
end
puts format("%-11s %-9s %s", key, value.inspect, follows)
end Output
word false YAML 1.1 octal 8 YAML 1.1 new_octal "0o10" YAML 1.1 base_60 5400 neither float "1e3" YAML 1.1 underscore 1000 YAML 1.1
Recorded with ruby 4.0.7 (revision 229531a6cf) +PRISM [arm64-darwin23] on macOS 26 arm64. To run it yourself: ruby probe.rb
Five of the six readings match YAML 1.1. The sixth, base_60, matches neither, because Psych’s 5400 is its own reading
of 1:30. Load the same probe with your own tool, print what it built, for example as JSON, and compare with the two
columns. For a tool you cannot script, such as a deployment system, look for a dry run or a command that prints the
settings it loaded.
The documentation of a library sometimes says the same in words, and where it does, it is worth reading before you probe:
| Library | What its documentation says |
|---|---|
yaml for JavaScript |
a version option, '1.2' by default with the core schema; '1.1' selects the yaml-1.1 schema |
| go-yaml, v3 | “supports most of YAML 1.2”, but YAML 1.1 booleans count when decoding into a typed bool, and octal 0777 is read per YAML 1.1 |
| ruamel.yaml | “a YAML 1.2 loader/dumper package for Python” |
| KYAML, Kubernetes | a subset of YAML in flow style, whose string values must all be double-quoted |
Checkpoint
Check: Probing a tool's rules
Defences: quote what must stay text, and check the types
There are two families of defence. Some work in the file, so that no parser has anything to guess; one works in the program, so that a wrong type cannot pass unnoticed whichever parser read the file. Here are the branch’s settings twice, plain and with quotes:
YAML (config.yaml) · traps/config.yaml
# Settings of the Pune branch, written without quotes
ships_to: [IN, NO]
mode: 0755
version: 3.10
opens: 09:30 YAML (config-quoted.yaml) · traps/config-quoted.yaml
# The same settings, with every value that must stay text in quotes
ships_to: ["IN", "NO"]
mode: "0755"
version: "3.10"
opens: "09:30" The program checks the type of every setting after loading, then shows what Psych writes when it saves strings that look like other types:
# Two defences: check the types after loading, and quote what must stay text.
require "yaml"
# The type every setting must have once it is loaded.
EXPECTED = { "ships_to" => [String], "mode" => String, "version" => String, "opens" => String }
def type_problems(data)
EXPECTED.flat_map do |key, type|
value = data[key]
if type.is_a?(Array)
value.each_with_index.reject { |item, _| item.is_a?(type.first) }
.map { |item, i| "#{key}[#{i}]: expected #{type.first}, got #{item.inspect} (#{item.class})" }
elsif !value.is_a?(type)
["#{key}: expected #{type}, got #{value.inspect} (#{value.class})"]
else
[]
end
end
end
%w[config.yaml config-quoted.yaml].each do |file|
problems = type_problems(YAML.safe_load(File.read(file)))
puts "#{file}: #{problems.empty? ? 'no problems' : "#{problems.size} problems"}"
problems.each { |problem| puts " #{problem}" }
end
# What Psych writes for strings that look like other types.
puts
puts YAML.dump({ "country" => "NO", "mode" => "0755", "new_octal" => "0o10", "size" => "1e3" }) Output
config.yaml: 4 problems ships_to[1]: expected String, got false (FalseClass) mode: expected String, got 493 (Integer) version: expected String, got 3.1 (Float) opens: expected String, got 34200 (Integer) config-quoted.yaml: no problems --- country: 'NO' mode: '0755' new_octal: 0o10 size: 1e3
Recorded with ruby 4.0.7 (revision 229531a6cf) +PRISM [arm64-darwin23] on macOS 26 arm64. To run it yourself: ruby defences.rb
The check found four problems in the plain file, each with its path and the type that arrived: Norway as false, the
mode as 493, the version as 3.1 and the opening time as 34200. The quoted file passed. A check like this catches traps
nobody listed, because it compares what arrived with what the program needs; a JSON Schema with "type": "string"
does the same job in any language.
The dump is a warning about writers. Psych quoted 'NO' and '0755', which it would itself read as other types, but
wrote 0o10 and 1e3 plain, because for Psych they are strings anyway. A YAML 1.2 parser reads those two as 8 and
1000.0, so a file written by a YAML 1.1 emitter is safe for YAML 1.1 readers only. KYAML takes the opposite approach:
flow style, in which every string value is double-quoted, so no reader has a value to guess.
Which defence fits depends on who writes the files and who reads them. Answer three questions about yours:
Answer the questions above: the table then marks what fits your case and says why the rest does not.
| Defence | Catches an unseen trap | Safe with YAML 1.1 readers | Easy for people to edit | Work to adopt |
|---|---|---|---|---|
| Quote values that must stay text Works in: the files | Mixed only the values someone thought to quote | Good a quoted scalar is a string to every parser: config-quoted.yaml passed | Mixed readable, but people add new lines without quotes | Good one pass over the files |
| Explicit tags such as !!str Works in: the files | Mixed only where a tag was written | Good !!str kept 010 and 09:30 as text in Psych | Poor a tag on every risky value is noise to read | Mixed every risky value needs its own tag |
| A type check after loading Works in: the program that reads | Good any wrong type fails loudly: 4 problems found in config.yaml | Good works whatever rules the parser follows | Good the files stay as people wrote them | Mixed a list of expected types to keep up to date |
| A YAML 1.2 parser Works in: the program that reads | Mixed removes the YAML 1.1 traps, but 3.10 is still 3.1 | Poor other programs may still read the same file with 1.1 rules | Good nothing changes in the files | Mixed a library change, and a comparison of old and new loads |
| KYAML style: every string quoted Works in: the files | Good every string value is double-quoted, so no value is guessed | Good still YAML, and quoted strings read the same everywhere | Mixed braces and quotes instead of the familiar block style | Mixed convert the files, or have a tool write them |
How the answers decide
- “Who writes the files?” People, by hand: adds a caveat to Quote values that must stay text, Explicit tags such as !!str and KYAML style: every string quoted. People add lines without quotes or tags and find braces tiring, so back hand-written files with a type check in CI.
- “Who writes the files?” Programs only: adds a caveat to Explicit tags such as !!str. A program can quote every string as it writes, which is simpler than tagging; Psych left 0o10 and 1e3 unquoted, so check its output.
- “Can you choose the parser of every program that reads the files?” No, some belong to others: rules out A YAML 1.2 parser. A YAML 1.2 parser in your program does not change how the other programs read the same file.
- “Do you know the type each setting must have?” No, the files vary too much: rules out A type check after loading. A type check needs the expected type of every setting; without one, quote what must stay text.
- “Do you know the type each setting must have?” Yes, or we can write it down: adds a caveat to A YAML 1.2 parser. With known types, a check catches what a parser change cannot: 3.10 is the float 3.1 in both versions.
In short: quote in the files whatever must stay text, back hand-written files with a type check after loading, and choose YAML 1.2 parsers for new programs, without counting on every reader having one.
Checkpoint
Check: Quotes and type checks as defences
Practise: write the YAML 1.1 rulebook
The exercise asks you to write the YAML 1.1 column as a function: the boolean words, octal, binary, base 60, floats that need a dot, and dates, with the traps of this lesson as its sample tests:
Exercise · Medium · Python
Resolve plain scalars by the YAML 1.1 type pages
Write resolve_11(text) in yaml11.py. It takes the text of a plain scalar and returns the tag and the value that the YAML 1.1 type pages give it, as a tuple. Try the rules in this order and use the first whose pattern matches the whole text; remove underscores before you convert a number:
| Rule | Pattern | Result | | --- | --- | --- | | null | ~, null, Null, NULL, or nothing at all | ("null", None) | | true | y, Y, yes, Yes, YES, true, True, TRUE, on, On, ON | ("bool", True) | | false | n, N, no, No, NO, false, False, FALSE, off, Off, OFF | ("bool", False) | | binary | [-+]?0b[0-1_]+ | ("int", …) | | octal | [-+]?0[0-7_]+ | ("int", …) | | decimal | [-+]?(0\|[1-9][0-9_]*) | ("int", …) | | hexadecimal | [-+]?0x[0-9a-fA-F_]+ | ("int", …) | | base 60 | [-+]?[1-9][0-9_]*(:[0-5]?[0-9])+ | ("int", …): each part times 60, plus the next | | float | [-+]?([0-9][0-9_]*)?\.[0-9_]*([eE][-+][0-9]+)? | ("float", …) | | infinity | [-+]?\.(inf\|Inf\|INF) | ("float", inf) with its sign | | not a number | \.(nan\|NaN\|NAN) | ("float", nan) | | date | [0-9]{4}-[0-9]{2}-[0-9]{2} | ("timestamp", text) | | anything else | | ("str", text) |
The float type page writes [0-9.]* after the dot, which would also match a version such as 1.4.2; this exercise allows digits and underscores there, so every match can be converted. Two forms of the type pages are left out: base-60 floats such as 190:20:30.15, and timestamps with a time of day. The sample tests are the traps of the lesson.
Starter code · yaml11.py
def resolve_11(text):
"""The (tag, value) that the YAML 1.1 type pages give the plain scalar text."""
return ("str", text) The sample tests · test_yaml11.py
import math
from yaml11 import resolve_11
def test_boolean_words():
"""reads the 22 boolean words in their listed capitals, and nothing else"""
assert resolve_11("NO") == ("bool", False)
assert resolve_11("y") == ("bool", True)
assert resolve_11("On") == ("bool", True)
assert resolve_11("off") == ("bool", False)
assert resolve_11("nO") == ("str", "nO")
assert resolve_11("yES") == ("str", "yES")
def test_octal_and_decimal():
"""reads a leading 0 as octal, and 08 as text because 8 is no octal digit"""
assert resolve_11("010") == ("int", 8)
assert resolve_11("0755") == ("int", 493)
assert resolve_11("08") == ("str", "08")
assert resolve_11("0") == ("int", 0)
assert resolve_11("+12") == ("int", 12)
def test_binary_hex_and_underscores():
"""reads 0b and 0x with signs and underscores"""
assert resolve_11("0b101") == ("int", 5)
assert resolve_11("1_000") == ("int", 1000)
assert resolve_11("0x_1F") == ("int", 31)
assert resolve_11("-0x1F") == ("int", -31)
def test_base_60():
"""reads base-60 integers, and refuses a part of 60 or a first part of 0"""
assert resolve_11("1:30") == ("int", 90)
assert resolve_11("22:22") == ("int", 1342)
assert resolve_11("190:20:30") == ("int", 685230)
assert resolve_11("1:60") == ("str", "1:60")
assert resolve_11("0:30") == ("str", "0:30")
def test_floats():
"""needs a dot, and a sign in the exponent"""
assert resolve_11("3.10") == ("float", 3.1)
assert resolve_11("1.5e+3") == ("float", 1500.0)
assert resolve_11(".5") == ("float", 0.5)
assert resolve_11("1e3") == ("str", "1e3")
assert resolve_11("1.0e3") == ("str", "1.0e3")
def test_special_floats_null_and_dates():
"""reads .inf, .nan, the null words and dates"""
assert resolve_11("-.inf") == ("float", -math.inf)
tag, value = resolve_11(".NaN")
assert tag == "float" and math.isnan(value)
assert resolve_11("~") == ("null", None)
assert resolve_11("") == ("null", None)
assert resolve_11("2026-10-04") == ("timestamp", "2026-10-04")
assert resolve_11("Pune") == ("str", "Pune") A hint
Keep the rules as (name, pattern) pairs in the order of the table and test each with re.fullmatch. For base 60, split on : and fold the parts: total = total * 60 + int(part) for each part, then apply the sign. Python's int(text, 8) reads an octal string with a leading 0, and int(text, 16) a hexadecimal one once 0x is removed.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Then go through booleans, numbers, keys, probes and defences once more. The questions pick the topic where your answers have been weakest, and each one shows the rule behind it:
Practice that adapts to you
Practise this lesson
11 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Warm-up (fresher to 2–5 years): what is the Norway problem in YAML?
Under the YAML 1.1 rules, the plain scalars yes, no, on, off and their capitalised forms are booleans, so the
country code NO, written without quotes, loads as false. Psych, the YAML parser in Ruby’s standard library, read
the list [IN, NP, LK, NO, SE, DK] as five strings and false, without an error. YAML 1.2’s core schema reads only
true and false as booleans, so a YAML 1.2 parser keeps NO as a string, but the same file may still be read by a
parser that follows YAML 1.1. The fix is to quote values that are codes or words, "NO", and to check the types
after loading, so that a boolean where a string belongs fails at once instead of surfacing later as a missing
country.
Two questions from real files follow: a port mapping that stopped working, and a version number that changed on its way through a release tool. Note what you would look at first, then compare with each first step:
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Fresher to mid
A service in a Compose-style file lists its ports as - 22:22 and - 8080:80. The second mapping works; the first one never does. Explain what happened, why only one entry is affected, and how you would fix the file and keep the trap out of new files.
Flashcards · 7 cards
Recall it, then check
Say the answer to yourself before you look: recalling it is what makes it stick.
-
What is the Norway problem?
Under YAML 1.1 rules yes, no, on and off are booleans, so the country code NO, unquoted, loads as false. YAML 1.2's core schema reads only true and false as booleans.
-
What does 0755 become under YAML 1.1 rules?
493, since a leading zero means octal there. The core schema reads it as 755, and spells octal 0o755.
-
What did Psych make of the plain value 22:22?
80520. Psych read two parts as hours and minutes and counted seconds, where YAML 1.1's base-60 rule gives 1342. Quote times and port mappings.
-
Why did the version 3.10 come back as 3.1?
Unquoted, it is a float under every YAML rulebook, and the float 3.10 is 3.1. Quote version numbers.
-
Why can a key named on turn into true?
Keys are coerced exactly like values, and under YAML 1.1 rules the plain word on is a boolean.
-
How can you tell which YAML rules a tool follows?
Feed it a small probe file of values whose type depends on the rulebook, such as NO, 010, 0o10 and 1:30, and look at what comes back.
-
Which two defences stop YAML type coercion?
Quote every value that must stay text, such as codes, versions, file modes and times, and check the types after loading so a wrong type fails at once.
Key takeaways
- Under YAML 1.1 rules
yes,no,onandoffare booleans, soNOloads asfalse; Psych reads those words in any capitals but keepsyandnas strings, and the core schema reads onlytrueandfalse. - A leading
0is octal in YAML 1.1 (0755is 493),22:22is a base-60 number (80520 in Psych),3.10is 3.1 everywhere, and Psych even reads1,00,000as 100000. - Keys are coerced like values, so a workflow’s
onkey becametrue; Psych builds aDatefrom a plain date when it is allowed to, and~is null. - A probe file of values that the versions read differently tells you which rules a tool follows; Psych matched YAML
1.1 on five of six and read
1:30its own way. - Quote what must stay text and check the types after loading; a file written by a YAML 1.1 emitter such as Psych can
still hold
0o10and1e3plain, which a YAML 1.2 reader turns into numbers.
Terms in this lesson
- base-60 integer
- A YAML 1.1 integer written in parts separated by colons, each worth 60 times the next, such as 1:30 for 90. YAML 1.2 dropped the form, so 1:30 is a string there.
- Norway problem
- The YAML 1.1 trap in which the plain scalar NO, the two-letter code for Norway, loads as the boolean false, because no, NO, yes, on and off are boolean words under the YAML 1.1 rules.
References
- Boolean Language-Independent Type for YAML Version 1.1 (YAML Language Development Team)
- Integer Language-Independent Type for YAML Version 1.1 (YAML Language Development Team)
- Floating-Point Language-Independent Type for YAML Version 1.1 (YAML Language Development Team)
- Timestamp Language-Independent Type for YAML Version 1.1 (YAML Language Development Team)
- YAML Specification Changes (YAML Language Development Team)
- YAML Ain't Markup Language (YAML) version 1.2.2, the core schema (YAML Language Development Team)
- psych/scalar_scanner.rb at v5.3.1: how Psych types plain values (Ruby)
- ISO 3166 Country Codes (ISO)
- Compose file reference: services, ports (Docker)
- Workflow syntax for GitHub Actions: on (GitHub)
- go-yaml: compatibility (The YAML Project)
- yaml: options (version, schema) (yaml (eemeli/yaml))
- ruamel.yaml on PyPI: project description (Python Package Index)
- KYAML (The Kubernetes Authors)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress