Why a Number Turns Into Something Else in YAML Files
Leading zeros, hex, base 60 times and trailing zeros change what a number is. Real output for 0755, 12:30, 1.10 and 1e3, and how to protect values.

"My config says 0755 and my program got 493." The first time it happens, nobody believes the parser. It is not a bug. YAML decides what an unquoted value means from its shape, and several shapes that look like plain numbers are something else. This post tests the shapes that cause trouble in two parsers that implement different versions of the language, and says exactly what each does.
The shapes that change meaning
Leading zeros (octal). In YAML 1.1, an integer that starts with 0 is octal. This was designed so that file permissions read naturally (mode: 0755). The result:
0755 PyYAML=493 ruamel=755
0o755 PyYAML='0o755' ruamel=493
755 PyYAML=755 ruamel=755
0777 PyYAML=511 ruamel=777
09 PyYAML='09' ruamel=9In PyYAML 0755 is 493, which is 755 written in base 8. In the 1.2 parser it is the decimal 755, and the 1.2 way to write octal is 0o755, which PyYAML does not recognize (it stays a string). 09 is the interesting one: 9 is not an octal digit, so PyYAML does not treat it as an integer and leaves the string '09', while the 1.2 parser reads the integer 9. A file with 0755 and 09 as zip codes, say, would produce a number in some rows and a string in others under one parser. Identifiers with leading zeros are always strings, whatever the parser does.
Hexadecimal. 0x1F is 31 in both parsers. Fine, unless the value was an identifier that happens to look like hex, such as 0xCAFE meant as text.
Base 60 (sexagesimal). In YAML 1.1 a colon between digit groups makes a base-60 number, meant for times and angles. The parts after the first must be below 60:
12:30 PyYAML=750 ruamel='12:30'
190:20:30 PyYAML=685230 ruamel='190:20:30'
22:22 PyYAML=1342 ruamel='22:22'
80:80 PyYAML='80:80' (not valid base 60)12:30 is 12 times 60 plus 30, which is 750, in PyYAML. A time of day in a config file becomes a count of seconds or minutes. The container port mapping 22:22 is 1342 in a 1.1 parser, and 80:80 stays text because 80 is not a valid base-60 digit group. The 1.2 parser keeps all of them as strings.
Trailing zeros. A decimal number is parsed as a float, and a float does not remember its trailing zeros:
1.10 PyYAML=1.1 ruamel=1.1
3.10 -> 3.1 in bothThis is why a version 3.10 becomes 3.1. Both parsers do it, so this one is not a 1.1 problem (YAML version numbers turning into floats).
Exponents and underscores. 1e3 stayed the string '1e3' in PyYAML, since YAML 1.1 requires a dot in a float, and became 1000.0 in the 1.2 parser. The underscore form 1_000 became the integer 1000 in both tests, which can turn an identifier written with separators into a number.
Special floats. .inf and .nan load as infinity and not-a-number in both, so a value spelled like that is never text.
Dates. An unquoted 2026-01-05 becomes a date object, and 2026-01-05 10:00:00 a datetime, in both parsers. JSON serialization of the loaded result then fails with "Object of type date is not JSON serializable", a common surprise in front-matter processing (reading YAML front matter from Markdown files).
Why it works this way
The rules come from the type resolution of each version. The parser tries patterns in order on a plain scalar: null, boolean, integer, float, timestamp, then string, and the first match wins. YAML 1.1's patterns are generous (octal, sexagesimal, many boolean words). YAML 1.2's core schema is stricter, close to JSON's number syntax plus a few additions. A quoted scalar skips resolution entirely. That is the mechanism behind every fix.
The fix, in order of reliability
Quote values that are identifiers, codes, versions or times.
"0755"(when a string is meant),"12:30","3.10","00123". Quotes make a string in every parser. This works regardless of version.For file modes, use the form your consumer expects. If a tool wants an octal integer and reads 1.1,
0755is right. If it reads 1.2, write0o755. Many tools avoid the question by taking a string ("0755") and parsing it themselves.Use a schema with types. If the field is declared a string, a number arriving fails loudly instead of flowing through. With pydantic, a
strfield rejected an integer, and with JSON Schema"type": "string"does the same (validating YAML configuration with pydantic).Prefer a 1.2 parser where you control the reader, knowing it changes the octal and boolean behavior for existing files.
Do not rely on a directive.
%YAML 1.2at the top of a file is honored inconsistently.
Checking an existing file
To find values that load as something other than text, load the file and print the types of the scalars you expect to be strings:
import yaml
def walk(n, path=""):
if isinstance(n, dict):
for k, v in n.items(): walk(v, f"{path}/{k}")
elif isinstance(n, list):
for i, v in enumerate(n): walk(v, f"{path}[{i}]")
elif not isinstance(n, str):
print(path, type(n).__name__, repr(n))
walk(yaml.safe_load(open("config.yaml")))Every line it prints is a scalar that is not a string. Scan the list for things that should be: ports as integers are fine, but a zip, an id or a version in the list is a finding. A stricter variant uses yaml.compose, which gives you the original text of each scalar, and flags values with leading zeros or ending in zero after a decimal point (YAML version numbers turning into floats shows that script).
Related reading
The cluster hub is fixing YAML errors that make no sense. For the boolean case, see the Norway problem and YAML boolean surprises, and for the version differences YAML 1.1 versus 1.2. The type patterns are in the YAML 1.1 integer type and the 1.2.2 core schema. See how a value loads in the YAML to JSON tool.
When quoting does not preserve a number's meaning
The consumer converts it back. A tool that reads "0755" as a string and calls an integer parser with base 10 gets 755, not the octal 493 you intended. Read the tool's documentation on file modes.
A formatter removes the quotes. Some formatters unquote strings they consider safe under 1.2, which a 1.1 reader then converts. Use a round-trip library that keeps quote style.
The type is checked later. If a downstream step converts all strings to numbers by pattern, the quotes you added are not enough.
The data is really numeric. A port or a count that is a number should stay unquoted, and a schema should state the type.


