Data Formats (JSON, YAML, TOML, XML, CSV) Module 6 – TOML 1.1 and other configuration formats
TOML 1.1, dates, integers and parsers
What TOML 1.1 added and which parsers read it, the four date and time types in Python and JavaScript, and 64-bit integers that a JavaScript number cannot hold.
What you will learn
- List what TOML 1.1.0 added to TOML 1.0.0, and check which version a parser reads
- Rewrite TOML 1.1.0 forms so that a parser of TOML 1.0.0 reads the same data
- Map TOML's four date and time types to Python and JavaScript values without losing an offset or a digit
- Read 64-bit TOML integers in JavaScript without losing digits, and write them to JSON safely
Before you start
What you’ll learn · Before you start
What you will learn
- List what TOML 1.1.0 added to TOML 1.0.0, and check which version a parser reads
- Rewrite TOML 1.1.0 forms so that a parser of TOML 1.0.0 reads the same data
- Map TOML's four date and time types to Python and JavaScript values without losing an offset or a digit
- Read 64-bit TOML integers in JavaScript without losing digits, and write them to JSON safely
Before you start
On this page
The Pune branch’s settings load with the tomllib of Python 3.15 and fail with the one of Python 3.14, and the first
line that Python 3.14 refuses is opens = 09:30, a time without its seconds. TOML 1.1.0 allows it, along with four
other forms that a TOML 1.0.0 parser refuses, so the version a parser reads now decides whether a file loads at all.
Values differ too: a JavaScript reader that turns order_id = 9007199254740993 into a number gets 9007199254740992.
You will list what TOML 1.1.0 added and which parsers read it, rewrite its new forms for older parsers, map the four date and time types to Python and JavaScript without losing an offset or a digit, and keep 64-bit integers exact in JavaScript and in JSON.
TOML 1.1 adds five forms that a 1.0 parser refuses
TOML 1.1.0 changed the syntax in five places. An inline table may now run over several lines, with a comment on any of
them and a comma after its last pair. Basic strings gain two escapes: \e for the escape character and \xHH for any
character up to code point 255. And the seconds of a time or a date-time become optional, so 09:30 means 09:30:00.
The other changes only clarify: comments never change the data a parser builds, and parsers may support integers and
floats of any size. Here are the Pune settings written with the new forms, and the same data in TOML 1.0.0:
TOML (toml11.toml) · versions/toml11.toml
# The Pune settings, written with TOML 1.1.0 syntax
branch = "Pune"
opens = 09:30
sale_starts = 2026-11-02 10:00
banner = "\e[1mShelf\e[0m"
sign = "Caf\xE9 corner"
server = {
host = "127.0.0.1", # only this machine
port = 8080,
} TOML (toml10.toml) · versions/toml10.toml
# The same settings in TOML 1.0.0, which every TOML parser reads
branch = "Pune"
opens = 09:30:00
sale_starts = 2026-11-02 10:00:00
banner = "\u001B[1mShelf\u001B[0m"
sign = "Café corner"
[server]
host = "127.0.0.1" # only this machine
port = 8080 This terminal session reads both files with two Pythons. python3.14 is Python 3.14.8, whose tomllib reads TOML
1.0.0; python3.15 is a release candidate of Python 3.15, whose tomllib reads TOML 1.1.0:
from session import run, version
# The same settings twice, once with TOML 1.1.0 syntax and once in TOML 1.0.0, read by the tomllib of Python 3.14
# (TOML 1.0.0) and of Python 3.15 (TOML 1.1.0).
version()
run(
"""
python3.14 read.py toml11.toml
python3.14 read.py toml10.toml
python3.15 read.py toml11.toml
python3.15 read.py toml10.toml
python3.15 same.py
""",
expect_failure=("python3.14 read.py toml11.toml",),
) Output
# python3.14: Python 3.14.8
# python3.15: Python 3.15.0rc3
$ python3.14 read.py toml11.toml
toml11.toml: Expected newline or end of document after a statement (at line 3, column 10)
[exit status 1]
$ python3.14 read.py toml10.toml
{"branch": "Pune", "opens": "09:30:00", "sale_starts": "2026-11-02 10:00:00", "banner": "\u001b[1mShelf\u001b[0m", "sign": "Café corner", "server": {"host": "127.0.0.1", "port": 8080}}
$ python3.15 read.py toml11.toml
{"branch": "Pune", "opens": "09:30:00", "sale_starts": "2026-11-02 10:00:00", "banner": "\u001b[1mShelf\u001b[0m", "sign": "Café corner", "server": {"host": "127.0.0.1", "port": 8080}}
$ python3.15 read.py toml10.toml
{"branch": "Pune", "opens": "09:30:00", "sale_starts": "2026-11-02 10:00:00", "banner": "\u001b[1mShelf\u001b[0m", "sign": "Café corner", "server": {"host": "127.0.0.1", "port": 8080}}
$ python3.15 same.py
toml10.toml and toml11.toml hold the same data: True
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 versions_session.py
Python 3.14 refused toml11.toml at line 3, opens = 09:30, and read toml10.toml. Python 3.15 read both and found
the same data in them, as the Python 3.15 documentation promises: a TOML 1.0.0 document reads the same way with a TOML
1.1.0 parser. Look at the message, too. It says nothing about versions, only “Expected newline or end of document after
a statement”, the message that copies = 007 got in the previous lesson. A TOML 1.0.0 parser has never heard of
TOML 1.1.0, so new syntax looks like a typo to it.
Which parsers read which version is a matter of record, event by event:
- TOML 1.0.0
The first stable version of TOML
The format of the previous lesson: tables, arrays of tables, inline tables on one line, four kinds of date and time, and integers that should be exact to 64 bits.
- Python 3.11
tomllib joins the standard library
Python adds tomllib, based on the tomli package, to read TOML 1.0.0. It has no writer. Python 3.11 to 3.14 all read TOML 1.0.0 only.
- TOML 1.1.0
Five new forms and some clarifications new syntax
Inline tables may span lines, with comments and a trailing comma; basic strings gain the \e and \xHH escapes; seconds become optional in times. Clarifications say comments never change the data and parsers may support any integer size.
- Cargo 1.94
Cargo reads TOML 1.1
Cargo parses TOML 1.1 in manifests and configuration files. Using the new forms in Cargo.toml raises the Rust version a project needs for development; the manifest Cargo publishes stays readable by older parsers.
- Python 3.15
tomllib reads TOML 1.1.0
The tomllib of Python 3.15 reads TOML 1.1.0, and every valid TOML 1.0.0 document is parsed the same way as before.
The timeline has 5 events, from TOML 1.0.0 to Python 3.15.
Beyond these, the tomli package, which tomllib was based on, has read TOML 1.1.0 since its version 2.4.0, and the
README of the JavaScript parser smol-toml calls it “fully spec-compliant with TOML v1.1.0”. To learn what a parser
reads, look for the TOML version in its documentation, or feed it one 1.1 form, such as t = 09:30, and see whether it
loads.
Write for the oldest parser that reads the file
A file is read by more programs than the one you test with. A pyproject.toml is read by the build tools and by every
tool whose [tool] table it holds, each on the Python of the developer who runs it. A tool’s own settings file is read
by that tool on its user’s computer. A Cargo.toml is read by the Cargo of every contributor, and Cargo’s changelog
warns that the new syntax raises the Rust version a project needs. If any of them may run a TOML 1.0.0 parser, the file
must be TOML 1.0.0, and every new form has a 1.0.0 spelling. This program tries each form on its own, then its 1.0.0
spelling; it runs the tomllib of Python 3.14, here and in the browser:
"""Each TOML 1.1.0 form on its own, then its TOML 1.0.0 spelling, read by this Python's tomllib."""
import tomllib
FORMS = [
("An inline table on three lines", 'server = {\n host = "127.0.0.1",\n port = 8080 }\n', 'server = { host = "127.0.0.1", port = 8080 }\n'),
("A trailing comma in an inline table", "point = { x = 1, y = 2, }\n", "point = { x = 1, y = 2 }\n"),
("A comment inside an inline table", "point = { x = 1, # across\n y = 2 }\n", "[point] # across\nx = 1\ny = 2\n"),
("The \\e escape", 'banner = "\\e[1mShelf"\n', 'banner = "\\u001B[1mShelf"\n'),
("A \\xHH escape", 'sign = "Caf\\xE9"\n', 'sign = "Caf\\u00E9"\n'),
("A time without seconds", "opens = 09:30\n", "opens = 09:30:00\n"),
("A date-time without seconds", "sale_starts = 2026-11-02 10:00\n", "sale_starts = 2026-11-02 10:00:00\n"),
]
for name, new, old in FORMS:
print(name)
for label, text in [(" 1.1 form:", new), (" 1.0 form:", old)]:
try:
print(label, tomllib.loads(text))
except tomllib.TOMLDecodeError as e:
print(label, "refused:", e) Output
An inline table on three lines
1.1 form: refused: Invalid initial character for a key part (at line 1, column 11)
1.0 form: {'server': {'host': '127.0.0.1', 'port': 8080}}
A trailing comma in an inline table
1.1 form: refused: Invalid initial character for a key part (at line 1, column 25)
1.0 form: {'point': {'x': 1, 'y': 2}}
A comment inside an inline table
1.1 form: refused: Invalid initial character for a key part (at line 1, column 18)
1.0 form: {'point': {'x': 1, 'y': 2}}
The \e escape
1.1 form: refused: Unescaped '\' in a string (at line 1, column 13)
1.0 form: {'banner': '\x1b[1mShelf'}
A \xHH escape
1.1 form: refused: Unescaped '\' in a string (at line 1, column 14)
1.0 form: {'sign': 'Café'}
A time without seconds
1.1 form: refused: Expected newline or end of document after a statement (at line 1, column 10)
1.0 form: {'opens': datetime.time(9, 30)}
A date-time without seconds
1.1 form: refused: Expected newline or end of document after a statement (at line 1, column 26)
1.0 form: {'sale_starts': datetime.datetime(2026, 11, 2, 10, 0)}
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 forms.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
Every 1.1 form was refused, and every 1.0 spelling gave the data the 1.1 form means: an inline table on one line,
\u001B for \e, \u00E9 for \xE9, and :00 for the missing seconds. A comment inside an inline table has no
place in TOML 1.0.0, so the 1.0 spelling turns the inline table into a [point] table, where the comment can stay on
the header line.
Move one form back to TOML 1.0.0
Press Edit and, in the first form, put the inline table on one line: server = { host = "127.0.0.1", port = 8080 }.
Run it, and the “1.1 form” line is read. Then add a comma before the closing brace and run again: it is refused.
A parser stops at the first error, so a TOML 1.0.0 parser finds one 1.1 form per run, as toml11.toml showed. In
CI, load every TOML file with a TOML 1.0.0 parser, such as tomllib on Python 3.14, whenever such a parser is among
the readers. A converter that reads both versions can list every 1.1 form of a file at once.
Key idea
A TOML file is only as new as the oldest parser that reads it: unless you control every reader, write TOML 1.0.0, and load the file with a TOML 1.0.0 parser in CI.
Checkpoint
Check: What TOML 1.1.0 added · Writing for TOML 1.0.0 parsers
Four kinds of date and time, and what each becomes
TOML has four date and time types, and the difference between them is what they promise. An
offset date-time: A TOML date and time with its offset from UTC, such as 10:00 at +05:30. It names one instant, the same on every computer, and Python's tomllib returns it as an aware datetime. such as 2026-11-02T10:00:00+05:30 names one instant, the same on
every computer. A local date-time: A TOML date and time with no offset. It is a reading of a wall clock, not an instant: computers in two time zones turn it into two different instants. such as 2026-11-02T09:30:00 is a reading of a
wall clock, with no zone. A local date is a day, and a local time is a time of day. Here is a one-day sale at the Pune
branch, with all four:
# A one-day sale at the Pune branch
starts = 2026-11-02T10:00:00+05:30
ends = 2026-11-02T15:30:00Z
reminder = 2026-11-02T09:30:00
day = 2026-11-02
doors_open = 09:45:00
receipt_cut = 2026-11-02T10:00:00.123456789+05:30 tomllib maps each type to a type of Python’s datetime module, and the program below shows what follows from that:
"""What tomllib builds from the four TOML date and time types, and what happens next in Python and in JSON."""
import json
import tomllib
from datetime import timedelta, timezone
with open("sale.toml", "rb") as f:
sale = tomllib.load(f)
for key, value in sale.items():
zone = getattr(value, "tzinfo", None)
print(f"{key:<12}{type(value).__name__:<9}{value.isoformat():<33}tzinfo={zone}")
# Nine digits of fraction: tomllib keeps six, the microseconds, and drops the rest without rounding.
print("receipt_cut microseconds:", sale["receipt_cut"].microsecond)
# An offset date-time is an instant; a local date-time is a wall-clock reading. Python will not compare the two.
try:
print(sale["reminder"] < sale["starts"])
except TypeError as e:
print("TypeError:", e)
# Attach the zone you mean, on purpose, before comparing.
ist = timezone(timedelta(hours=5, minutes=30))
reminder = sale["reminder"].replace(tzinfo=ist)
print("reminder before starts:", reminder < sale["starts"], "by", sale["starts"] - reminder)
# JSON has no date type: isoformat() keeps each value's own form, offset included.
print(json.dumps(sale, default=lambda v: v.isoformat())) Output
starts datetime 2026-11-02T10:00:00+05:30 tzinfo=UTC+05:30
ends datetime 2026-11-02T15:30:00+00:00 tzinfo=UTC
reminder datetime 2026-11-02T09:30:00 tzinfo=None
day date 2026-11-02 tzinfo=None
doors_open time 09:45:00 tzinfo=None
receipt_cut datetime 2026-11-02T10:00:00.123456+05:30 tzinfo=UTC+05:30
receipt_cut microseconds: 123456
TypeError: can't compare offset-naive and offset-aware datetimes
reminder before starts: True by 0:30:00
{"starts": "2026-11-02T10:00:00+05:30", "ends": "2026-11-02T15:30:00+00:00", "reminder": "2026-11-02T09:30:00", "day": "2026-11-02", "doors_open": "09:45:00", "receipt_cut": "2026-11-02T10:00:00.123456+05:30"}
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 dates.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
The offset date-times came back as aware datetime: A Python datetime whose tzinfo is set, so it names one instant. A naive datetime has no tzinfo, and Python refuses to compare the two kinds for order. datetimes, with tzinfo set to UTC+05:30
or UTC; the local date-time came back naive, with no tzinfo, and the local date and time as a date and a time.
The specification asks for at least millisecond precision and says that extra digits “must be truncated, not
rounded”: tomllib kept six of the nine digits of receipt_cut, 123456, where rounding would have given 123457.
Then Python refused to compare the naive reminder with the aware start, with a TypeError, because a wall-clock
reading is not an instant. Attaching the zone on purpose, UTC+05:30, made them comparable: the reminder comes 30
minutes before the sale. Last, JSON has no date type, and isoformat() wrote each value in its own form, offsets
included.
JavaScript has one date type, Date, which holds an instant in whole milliseconds. This program gives it the four
values twice, first with the time zone set to Indian time and then to German time, as two servers would be:
// What JavaScript's Date makes of the sale's four kinds of TOML value, on computers in two time zones.
// A JavaScript TOML parser has to return something for each of them; Date alone cannot hold all four.
const values = {
starts: "2026-11-02T10:00:00+05:30", // offset date-time
reminder: "2026-11-02T09:30:00", // local date-time
day: "2026-11-02", // local date
doors_open: "09:45:00", // local time
};
for (const zone of ["Asia/Kolkata", "Europe/Berlin"]) {
process.env.TZ = zone; // Node.js reads the time zone again when TZ changes
console.log(zone);
for (const [key, text] of Object.entries(values)) {
const d = new Date(text);
console.log(` ${key.padEnd(11)}${Number.isNaN(d.getTime()) ? "Invalid Date" : d.toISOString()}`);
}
}
// A Date holds whole milliseconds, so a longer fraction is cut, not rounded.
console.log(new Date("2026-11-02T10:00:00.123456789+05:30").toISOString()); Output
Asia/Kolkata starts 2026-11-02T04:30:00.000Z reminder 2026-11-02T04:00:00.000Z day 2026-11-02T00:00:00.000Z doors_open Invalid Date Europe/Berlin starts 2026-11-02T04:30:00.000Z reminder 2026-11-02T08:30:00.000Z day 2026-11-02T00:00:00.000Z doors_open Invalid Date 2026-11-02T04:30:00.123Z
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node dates.mjs
Only the offset date-time came out the same in both zones. The local date-time became 04:00 UTC in Kolkata and 08:30
UTC in Berlin, so the Berlin reminder would fire four hours after the sale began. The local date became midnight UTC in
both, because, as MDN puts it, date-only forms “are interpreted as a UTC time” while date-time forms are read as local
time. The local time was not a date at all, and the nine-digit fraction was cut to three. A JavaScript TOML parser
therefore needs types of its own: the smol-toml README describes TomlDate, which keeps the offset at millisecond
precision, and an option that returns the Temporal types PlainDateTime, PlainDate, PlainTime and
ZonedDateTime instead, in runtimes that have Temporal. In short, the four types arrive like this:
| TOML type | Python, from tomllib |
JavaScript Date |
|---|---|---|
| Offset date-time | Aware datetime |
The same instant everywhere |
| Local date-time | Naive datetime |
An instant that depends on the computer’s zone |
| Local date | date |
Midnight UTC |
| Local time | time |
Invalid Date |
A local date-time for an instant
Wrong: starts = 2026-11-02T10:00:00 for a sale that starts at one moment for everyone; servers in two zones read
two instants. Right: starts = 2026-11-02T10:00:00+05:30. Keep the local types for wall-clock values, such as the
hours a branch opens, which mean the same local time wherever they are read.
Large integers: TOML keeps them exact, a JavaScript number cannot
TOML 1.0.0 says 64-bit integers “should be accepted and handled losslessly”, and both versions say an integer that
cannot be represented losslessly must be an error, never a rounded value. TOML 1.1.0 adds that parsers may support any
integer size. Python’s int has no size limit, so tomllib keeps every digit, even past 64 bits:
# Order numbers from the Pune branch's old system
small = 2_500
largest_safe = 9007199254740991
order_id = 9007199254740993
int64_max = 0x7FFF_FFFF_FFFF_FFFF
past_int64 = 9223372036854775808 """tomllib on large integers: Python's int has no size limit, so tomllib keeps every digit, even past 64 bits."""
import json
import tomllib
INT64 = range(-(2**63), 2**63)
SAFE = range(-(2**53 - 1), 2**53)
with open("ids.toml", "rb") as f:
ids = tomllib.load(f)
for key, value in ids.items():
print(f"{key:<13}{value:<22}fits 64 bits: {value in INT64!s:<6}safe in JavaScript: {value in SAFE}")
# json.dumps writes every digit; a JavaScript reader of this text would still round order_id with JSON.parse.
print(json.dumps(ids)) Output
small 2500 fits 64 bits: True safe in JavaScript: True
largest_safe 9007199254740991 fits 64 bits: True safe in JavaScript: True
order_id 9007199254740993 fits 64 bits: True safe in JavaScript: False
int64_max 9223372036854775807 fits 64 bits: True safe in JavaScript: False
past_int64 9223372036854775808 fits 64 bits: False safe in JavaScript: False
{"small": 2500, "largest_safe": 9007199254740991, "order_id": 9007199254740993, "int64_max": 9223372036854775807, "past_int64": 9223372036854775808}
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 ints.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
Every value came through whole, past_int64 included, which is 2⁶³ and does not fit in 64 bits: a parser built on
64-bit integers must refuse it, so a file that tomllib accepts may still fail elsewhere. And json.dumps wrote every
digit, but the receiver decides what the digits become. A JavaScript number is a binary64: The IEEE 754 64-bit floating-point format, also called double precision, with 53 bits of precision. Every JavaScript number and every Python float is a binary64 value. More in “Numbers: precision, big integers and decimals”
value, exact only up to the largest safe integer: An integer that binary64 holds exactly and shares with no other integer: from -(2^53 - 1) to 2^53 - 1, which is 9007199254740991. Number.isSafeInteger tests for it. More in “Numbers: precision, big integers and decimals”, 9007199254740991.
This program compares Number() with a small reader that follows TOML’s rule: a Number while the value is safe, a
BigInt: JavaScript's type for whole numbers of any size, written with a trailing n, as 9007199254740993n. It does not mix with Number in arithmetic, and JSON.stringify refuses it. past that, and an error past 64 bits. Then it sends one order to JSON:
// TOML integers in JavaScript: what Number() makes of them, and a reader that follows TOML's rule instead
// (64-bit integers kept exactly, an error when a value cannot be kept).
const INT64_MIN = -(2n ** 63n);
const INT64_MAX = 2n ** 63n - 1n;
// Reads one TOML integer: decimal with an optional sign, or 0x, 0o, 0b; underscores only between digits.
function readTomlInteger(text, { bigint = "as-needed" } = {}) {
const m = /^([+-]?)(?:(0x)([0-9A-Fa-f](?:_?[0-9A-Fa-f])*)|(0o)([0-7](?:_?[0-7])*)|(0b)([01](?:_?[01])*)|(0|[1-9](?:_?\d)*))$/.exec(text);
if (!m || (m[1] && !m[8])) throw new SyntaxError(`not a TOML integer: ${text}`);
const digits = (m[3] ?? m[5] ?? m[7] ?? m[8]).replaceAll("_", "");
const prefix = m[2] ?? m[4] ?? m[6] ?? "";
let value = BigInt(prefix + digits);
if (m[1] === "-") value = -value;
if (value < INT64_MIN || value > INT64_MAX) throw new RangeError(`${text} does not fit in 64 bits`);
const safe = value >= BigInt(Number.MIN_SAFE_INTEGER) && value <= BigInt(Number.MAX_SAFE_INTEGER);
if (bigint === "always") return value;
if (safe) return Number(value);
if (bigint === "never") throw new RangeError(`${text} cannot be a Number without losing digits`);
return value;
}
const samples = ["2_500", "9007199254740991", "9007199254740993", "0x7FFF_FFFF_FFFF_FFFF", "9223372036854775808"];
for (const text of samples) {
const naive = Number(text.replaceAll("_", ""));
let read;
try {
const v = readTomlInteger(text);
read = typeof v === "bigint" ? `${v}n (BigInt)` : `${v} (Number)`;
} catch (e) {
read = `${e.name}: ${e.message}`;
}
console.log(`${text.padEnd(22)} Number(): ${String(naive).padEnd(20)} TOML reader: ${read}`);
}
try {
readTomlInteger("9007199254740993", { bigint: "never" });
} catch (e) {
console.log(`numbers only: ${e.name}: ${e.message}`);
}
// JSON.stringify refuses a BigInt; a replacer can write it as a string, which the reader must then expect.
const order = { order_id: readTomlInteger("9007199254740993"), qty: readTomlInteger("2") };
try {
JSON.stringify(order);
} catch (e) {
console.log(`${e.name}: ${e.message}`);
}
console.log(JSON.stringify(order, (key, v) => (typeof v === "bigint" ? v.toString() : v))); Output
2_500 Number(): 2500 TOML reader: 2500 (Number)
9007199254740991 Number(): 9007199254740991 TOML reader: 9007199254740991 (Number)
9007199254740993 Number(): 9007199254740992 TOML reader: 9007199254740993n (BigInt)
0x7FFF_FFFF_FFFF_FFFF Number(): 9223372036854776000 TOML reader: 9223372036854775807n (BigInt)
9223372036854775808 Number(): 9223372036854776000 TOML reader: RangeError: 9223372036854775808 does not fit in 64 bits
numbers only: RangeError: 9007199254740993 cannot be a Number without losing digits
TypeError: Do not know how to serialize a BigInt
{"order_id":"9007199254740993","qty":2}
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node ints.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
Number() turned 9007199254740993 into 9007199254740992, a different order, without a word, and both 64-bit values
into the same 9223372036854776000. The reader kept them as BigInts, refused 9223372036854775808 with a RangeError
as a 64-bit parser should, and in its “numbers only” mode refused the order id rather than round it. JavaScript TOML
parsers make the same choice through options: the smol-toml README says that by default it parses integers as plain
numbers and “is not able to parse integers beyond 53 bits”, and its integersAsBigInt option returns BigInts for
every integer or, set to 'asNeeded', only where a number cannot hold the value.
JSON.stringify refuses a BigInt with a TypeError. The replacer in the last line wrote the order id as a string,
"9007199254740993", which every JSON reader keeps, as long as the reader knows to expect a string.
Key idea
Read large TOML integers in JavaScript as BigInt, or refuse them, never as numbers, and send them to JSON as strings that the receiver expects.
Checkpoint
Check: The four date and time types · Large integers in JavaScript and JSON
Practise: send the sale to a JavaScript page
The exercise turns what tomllib returned into values that JSON can carry to a JavaScript page without a rounded id or
a lost offset. Its sample tests use the sale and the order ids of this lesson:
Exercise · Medium · Python
Make tomllib's values safe for JSON and JavaScript
A JavaScript page shows the Pune sale, and a Python service sends it the settings that tomllib read, as JSON. json.dumps refuses dates and times, and the page's JSON.parse would round large integers. Write json_safe(value) in jsonsafe.py. It takes what tomllib returned and gives back a copy that json.dumps can write and a JavaScript reader can read without losing anything:
- A dict or a list: the same shape, with every value inside made safe too.
- A
datetime,dateortime: itsisoformat()string, so an offset date-time keeps its offset and a local one stays without. - An integer outside the safe range, below −9,007,199,254,740,991 or above 9,007,199,254,740,991: its digits as a string. Integers inside the range stay integers.
TrueandFalsestay booleans.- A float that is
inf,-infornan: raiseValueError, because JSON has no way to write it. Other floats stay as they are. - Strings stay as they are.
Starter code · jsonsafe.py
def json_safe(value):
"""A copy of value, as tomllib returned it, that json.dumps can write and JavaScript can read without loss."""
return value The sample tests · test_jsonsafe.py
import datetime
import json
import math
from jsonsafe import json_safe
IST = datetime.timezone(datetime.timedelta(hours=5, minutes=30))
def test_dates_and_times():
"""each of the four TOML date and time types becomes its isoformat() string"""
assert json_safe(datetime.datetime(2026, 11, 2, 10, 0, tzinfo=IST)) == "2026-11-02T10:00:00+05:30"
assert json_safe(datetime.datetime(2026, 11, 2, 9, 30)) == "2026-11-02T09:30:00"
assert json_safe(datetime.date(2026, 11, 2)) == "2026-11-02"
assert json_safe(datetime.time(9, 45)) == "09:45:00"
def test_large_integers_become_strings():
"""integers past the safe range keep every digit as a string"""
assert json_safe(9007199254740993) == "9007199254740993"
assert json_safe(-9007199254740993) == "-9007199254740993"
assert json_safe(9223372036854775807) == "9223372036854775807"
def test_safe_integers_stay():
"""integers inside the safe range, its two ends included, stay integers"""
assert json_safe(2500) == 2500
assert json_safe(9007199254740991) == 9007199254740991
assert json_safe(-9007199254740991) == -9007199254740991
def test_booleans_stay():
"""True and False stay booleans"""
assert json_safe(True) is True
assert json_safe(False) is False
def test_inf_and_nan_are_refused():
"""inf, -inf and nan raise ValueError; other floats stay"""
for bad in (math.inf, -math.inf, math.nan):
try:
json_safe(bad)
except ValueError:
continue
raise AssertionError(f"{bad} was accepted")
assert json_safe(4.5) == 4.5
def test_nested_settings():
"""dicts and lists are copied with every value made safe, and json.dumps writes the result"""
sale = {
"starts": datetime.datetime(2026, 11, 2, 10, 0, tzinfo=IST),
"orders": [{"id": 9007199254740993, "qty": 2, "paid": True}],
"branch": "Pune",
}
assert json.dumps(json_safe(sale)) == (
'{"starts": "2026-11-02T10:00:00+05:30", "orders": [{"id": "9007199254740993", "qty": 2, "paid": true}], '
'"branch": "Pune"}'
) A hint
Handle the containers first (a dict comprehension and a list comprehension that call json_safe again), then the dates, then the numbers. Test for bool before int: in Python, True is an int too. math.isfinite tells you whether a float is inf or nan.
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 check yourself on the new forms, the dates and the large integers. Each wrong choice names the slip behind it, and the session keeps coming back to the idea you miss most:
Practice that adapts to you
Practise this lesson
15 questions about 4 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
A warm-up for freshers and 2–5 years: what did TOML 1.1 change, and does a TOML 1.0 file still read the same?
TOML 1.1.0 added five forms: an inline table may span lines, hold comments and end with a trailing comma; basic
strings gain the \e and \xHH escapes; and the seconds of a time or date-time are optional, so 09:30 is 09:30:00.
It also clarified that comments never change the data and that parsers may support integers and floats of any size.
Every valid TOML 1.0.0 file is still valid and reads the same way, as the Python 3.15 documentation says of its
tomllib. The reverse is not true: a TOML 1.0.0 parser, such as tomllib on Python 3.14, refuses every new form,
with messages that look like typos. So a file that older parsers must read keeps to TOML 1.0.0.
Three harder questions start from this lesson: a JavaScript service that misses an order, a reminder read in two time zones, and a tool whose users run five versions of Python. Sketch your answer to each before you open its first step:
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Mid to senior
A Node.js service reads order ids from a TOML file and looks them up in a database. Most lookups work, but order 9007199254740993 is never found, and no error appears anywhere. What happened, and what do you change in the parser call and in the data path?
-
Mid to senior
A TOML file gives a sale's start as a local date-time, with no offset, and two services send reminders from it, one in Mumbai and one in Frankfurt. The customers of one service get their reminder hours late. Why, and how do you fix the file and the code?
-
Mid to senior
Your command-line tool reads its settings file with tomllib, and its users run Python 3.11 to 3.15. A contributor wants to allow multi-line inline tables, because Python 3.15 reads them. What do you decide, and how do you make the decision stick?
Flashcards · 7 cards
Recall it, then check
Say the answer to yourself before you look: recalling it is what makes it stick.
-
What did TOML 1.1.0 add?
Inline tables over several lines with comments and a trailing comma, the \e and \xHH escapes, and times without seconds, so 09:30 means 09:30:00.
-
What does a TOML 1.0.0 parser do with the TOML 1.1.0 forms?
It refuses each, with a message that looks like a typo, as tomllib on Python 3.14 did. Every TOML 1.0.0 file still reads the same under 1.1.0.
-
Which TOML version should a file shared by many tools use?
TOML 1.0.0, unless you control every parser, with a CI step that loads the file with a TOML 1.0.0 parser.
-
What are TOML's four date and time types, and what does tomllib return?
Offset date-time, local date-time, local date and local time: an aware datetime, a naive datetime, a date and a time.
-
Why is a TOML local date-time a trap in JavaScript?
It is a wall-clock reading, not an instant, and a JavaScript Date turns it into a different instant in each time zone.
-
How much of a TOML time's fraction survives parsing?
What the parser can hold, and the rest is cut off: tomllib keeps microseconds, a JavaScript Date milliseconds.
-
How should JavaScript read a TOML integer above 9007199254740991?
As a BigInt, or not at all, never as a Number; then send it to JSON as a string the receiver expects.
Key takeaways
- TOML 1.1.0 added multi-line inline tables with comments and trailing commas, the
\eand\xHHescapes and optional seconds; a TOML 1.0.0 parser such astomllibon Python 3.14 refuses each of them with messages that look like typos. - A file is only as new as its oldest reader: write TOML 1.0.0 unless you control every parser, and check with a TOML 1.0.0 parser in CI.
- An offset date-time is an instant and a local date-time a wall-clock reading;
tomllibreturns them as aware and naive datetimes, and JavaScript’sDatereads the local one differently in every time zone. - Precision beyond what a parser holds is truncated:
tomllibkeeps microseconds and a JavaScriptDatekeeps milliseconds. tomllibkeeps every digit of an integer, but a JavaScript number is exact only to 9007199254740991: read larger integers as BigInt and send them to JSON as strings.
Terms in this lesson
- aware datetime
- A Python datetime whose tzinfo is set, so it names one instant. A naive datetime has no tzinfo, and Python refuses to compare the two kinds for order.
- BigInt
- JavaScript's type for whole numbers of any size, written with a trailing n, as 9007199254740993n. It does not mix with Number in arithmetic, and JSON.stringify refuses it.
- local date-time
- A TOML date and time with no offset. It is a reading of a wall clock, not an instant: computers in two time zones turn it into two different instants.
- offset date-time
- A TOML date and time with its offset from UTC, such as 10:00 at +05:30. It names one instant, the same on every computer, and Python's tomllib returns it as an aware datetime.
References
- TOML v1.1.0 (TOML)
- TOML v1.0.0 (TOML)
- TOML changelog (TOML)
- tomllib: Parse TOML files (Python 3.14) (Python Software Foundation)
- tomllib: Parse TOML files (Python 3.15) (Python Software Foundation)
- What's New In Python 3.15: tomllib (Python Software Foundation)
- PEP 680: tomllib, support for parsing TOML in the standard library (Python Software Foundation)
- Cargo changelog: Cargo 1.94 (The Rust Project)
- tomli (Python Package Index) (tomli project)
- smol-toml README (smol-toml project)
- datetime: aware and naive objects (Python 3.14) (Python Software Foundation)
- Date (MDN Web Docs) (Mozilla)
- BigInt (MDN Web Docs) (Mozilla)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress