Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

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.

  • Intermediate
  • 15 minutes
  • Examples run with Node.js 24.21.0, quickjs 0.32.0, Python 3.14.8, Pyodide 314.0.7, Ruby 4.0.7 and sql.js 1.14.2
  • By MySmartCoPilot
Free lesson, Pro answers: 15 minutes
Free: the whole lesson. With a pass: the model answer to its interview question.

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.yaml: two-letter codes, plain and quoted YAML · traps/countries.yaml
# 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, and eleven boolean words under three rulebooks Ruby · traps/booleans.rb
# 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

  1. Question 1 of 2 What did Psych return for ships_to: [IN, NP, LK, NO, SE, DK]?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Five strings and false in the place of NO

    YAML 1.1 lists 22 boolean words, NO among them. Quote codes and words, ["IN", "NO"], so they are strings under every rulebook.

  2. Question 2 of 2 What does Psych 5.3.1 make of gift_wrap: Y?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The string "Y"

    Psych reads yes, no, on, off, true and false in any capitals but leaves y and n as text. The same file can therefore mean true to one YAML 1.1 parser and "Y" to another, and the core schema reads "Y" too.

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:

rules.rb: the two rulebooks as patterns Ruby · traps/rules.rb
# 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:

Fourteen numbers that are not what they look like Ruby · traps/numbers.rb
# 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 0 means base 8, so 010 is 8 and the file mode 0755 is 493. The core schema reads them as 10 and 755 and wants 0o755 for octal, which YAML 1.1 keeps as a string. 08 shows 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:30 is 90 and 190:20:30 is 685230. Psych goes further: it reads two parts as hours and minutes, so 1:30 became 5400 and 22:22 became 80520 rather than the 1342 of the type page. Docker’s Compose reference says a port mapping HOST:CONTAINER “should always be specified as a (quoted) string” for this reason.
  • Binary and underscores. 0b101 and 1_000 are numbers only in YAML 1.1; the changes page says YAML 1.2 dropped binary and underscores.
  • Floats. 3.10 is the float 3.1 under every rulebook, so a version number loses its zero everywhere. 1e3 is 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,000 matches 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

  1. Question 1 of 2 Psych reads mode: 0755. Which integer does mode hold?

    Type a number.

    A hint

    A leading 0 means base 8 under YAML 1.1.

    Show the answer: Question 1 of 2

    Answer: 493

    0755 in base 8 is 7 × 64 + 5 × 8 + 5 = 493, the number numbers.rb printed for Psych and the YAML 1.1 pages. The core schema reads 755, so quote a file mode, or write 0o755 for a YAML 1.2 reader.

  2. Question 2 of 2 A Compose-style file lists ports: [22:22, 8080:80]. What does Psych build for the two entries?

    Choose one answer.

    A hint

    Each part after a colon of a base-60 number is at most 59, in one or two digits.

    Show the answer: Question 2 of 2

    Answer: The integer 80520, then the string "8080:80"

    Small port numbers after the colon fit the base-60 pattern; larger ones do not. That is why one mapping breaks and another does not, and why the Compose reference says HOST:CONTAINER should always be a quoted string.

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”.

workflow.yaml: a trigger key named on YAML · traps/workflow.yaml
# 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, dates and nulls read by Psych Ruby · traps/keys.rb
# 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

  1. Question 1 of 2 Psych reads workflow.yaml, whose trigger key is on. What does workflow["on"] return?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: nil, because the key was loaded as true

    Keys are plain scalars and follow the same rules as values. Quote keys that are words or codes, "on", when a file may be read with YAML 1.1 rules.

  2. Question 2 of 2 Why is a coerced key harder to find than a coerced value?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: It changes the shape of the data: lookups by the expected key miss, and the value seems to be absent

    A wrong value is visible where it is used; a wrong key makes the data look incomplete. When a setting seems to be missing, print the keys the parser built.

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 per rule the versions disagree on YAML · traps/probe.yaml
# 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:

Which rules does Psych follow? Ruby · traps/probe.rb
# 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
YAML Validator & Formatter Paste a file to list every plain value that YAML 1.1 and YAML 1.2 parsers read differently, with its line.

Checkpoint

Check: Probing a tool's rules

  1. Question 1 of 2 Psych read the probe value base_60: 1:30 as 5400. What does that tell you?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: On this value Psych follows neither rulebook: YAML 1.1 says 90 and YAML 1.2 says the string

    A probe tells you what a parser does, which can differ from both specifications. Test the values your files really contain, not only the ones the rulebooks predict.

  2. Question 2 of 2 go-yaml v3 decodes enabled: yes into a Go field of type bool, and label: yes into a field of type string. What does its documentation lead you to expect?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: true for the bool field and the string "yes" for the string field

    Some libraries mix the versions: go-yaml v3 follows most of YAML 1.2 but keeps YAML 1.1 booleans for typed bool targets and 0777 octal. Read the documentation, then probe.

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:

The same settings, plain and quoted

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:

A type check after loading, and what Psych writes Ruby · traps/defences.rb
# 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:

Interactive · answer and compare Which defence fits your files? Answer three questions about the files you look after. The table marks the defences that fit and says why the others fall short.
Who writes the files?
Can you choose the parser of every program that reads the files?
Do you know the type each setting must have?

Answer the questions above: the table then marks what fits your case and says why the rest does not.

Five defences against YAML type traps, against the four things that decide between them
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

  1. Question 1 of 2 The type check of defences.rb found four problems in config.yaml, though nobody had listed those traps. Why?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: It compares the type that arrived with the type the program needs, so it does not depend on knowing the traps

    A check of the loaded data catches every wrong type, known or not. A JSON Schema with "type": "string" for each text setting does the same in any language.

  2. Question 2 of 2 A Ruby service saves settings with YAML.dump, and a service with a YAML 1.2 parser reads the file. Which of these strings arrive as something else? Choose every one.

    Choose every answer that is right.

    Show the answer: Question 2 of 2

    Answer:

    • "1e3"
    • "0o10"

    An emitter quotes what its own rules would misread. A YAML 1.1 emitter does not know the YAML 1.2 forms, so a file it writes is safe for YAML 1.1 readers only.

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.

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.

  1. Question 1 of 11 Which of these plain scalars are booleans under the YAML 1.2 core schema? Choose every one.

    Choose every answer that is right.

    Show the answer: Question 1 of 11

    Answer:

    • FALSE
    • true

    The core schema reads true and false, each in three capitalisations, and nothing else, as the changes page says.

  2. Question 2 of 11 Which way of writing Norway's code reads as the string NO under every rulebook?

    Choose one answer.

    Show the answer: Question 2 of 11

    Answer: "NO"

    Quoting is the one spelling every rulebook agrees on. !!str NO would also work, at the cost of more noise.

  3. Question 3 of 11 What does version: 3.10 become under the core schema?

    Choose one answer.

    Show the answer: Question 3 of 11

    Answer: The float 3.1

    Version numbers are text that happens to contain digits and dots. Quote them, or the release that follows 3.9 can come out as 3.1.

  4. Question 4 of 11 Why is 1e3 a string under the YAML 1.1 rules and in Psych?

    Choose one answer.

    Show the answer: Question 4 of 11

    Answer: The YAML 1.1 float pattern needs a dot, and a sign before the exponent

    The two versions write floats differently: YAML 1.1 wants a dot and a signed exponent, the core schema makes both optional. The same text is therefore a string in one and 1000.0 in the other.

  5. Question 5 of 11 Which spelling of the file mode is 493 under the core schema but stays a string under Psych?

    Choose one answer.

    Show the answer: Question 5 of 11

    Answer: 0o755

    No plain octal spelling means the same to both versions. Write the mode as a quoted string that the program converts, or as the decimal 493, which every rulebook reads the same way.

  6. Question 6 of 11 Psych reads NO: Oslo. What is the key of that entry?

    Choose one answer.

    Show the answer: Question 6 of 11

    Answer: false

    The capitals came back as {"IN" => "New Delhi", false => "Oslo", "SE" => "Stockholm"}. Quote keys that are codes.

  7. Question 7 of 11 A setting must hold the text null, a code that a partner system uses. How do you write it?

    Choose one answer.

    Show the answer: Question 7 of 11

    Answer: code: "null"

    null, Null, NULL, ~ and the empty value are all null. Text that only looks like them needs quotes.

  8. Question 8 of 11 What is a probe file for?

    Choose one answer.

    Show the answer: Question 8 of 11

    Answer: Finding out which rules a tool's parser follows, by loading values that YAML 1.1 and 1.2 read differently

    Six values were enough to see that Psych follows YAML 1.1 on five of them and has its own reading of base 60.

  9. Question 9 of 11 A tool loads probe.yaml and reports word: "no", octal: 10 and new_octal: 8. Which rules does it follow?

    Choose one answer.

    Show the answer: Question 9 of 11

    Answer: The YAML 1.2 core schema

    Compare each reading with both columns of the probe. Load the rest of the probe too: a tool can follow YAML 1.2 for some values and keep a YAML 1.1 habit for others, as go-yaml v3 documents.

  10. Question 10 of 11 People edit 200 settings files by hand, and three programs with different parsers read them. Which plan fits best?

    Choose one answer.

    Show the answer: Question 10 of 11

    Answer: Quote every value that must stay text, and add a type check of the loaded settings in CI

    Hand-written files need a defence in the file that every parser respects and a check that does not depend on people remembering, which is what the table of defences recommends for files people write.

  11. Question 11 of 11 Which JSON Schema keyword makes a check refuse country: NO loaded as false?

    Choose one answer.

    Show the answer: Question 11 of 11

    Answer: "type": "string" on country

    A type check on the loaded data is the defence that catches traps nobody listed. In Ruby, defences.rb did the same with a table of expected classes.

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.

  1. 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.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

Flashcards · 7 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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.

  7. 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, on and off are booleans, so NO loads as false; Psych reads those words in any capitals but keeps y and n as strings, and the core schema reads only true and false.
  • A leading 0 is octal in YAML 1.1 (0755 is 493), 22:22 is a base-60 number (80520 in Psych), 3.10 is 3.1 everywhere, and Psych even reads 1,00,000 as 100000.
  • Keys are coerced like values, so a workflow’s on key became true; Psych builds a Date from 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:30 its 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 0o10 and 1e3 plain, 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

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.