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

  • Intermediate
  • 20 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: 20 minutes
Free: the whole lesson. With a pass: the model answers to its 3 interview questions.

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:

The Pune settings in TOML 1.1.0 and 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:

Two Pythons, two TOML versions, one file each Python · versions/versions_session.py
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:

Timeline · 5 events in order TOML versions, and when these parsers read them Walk through the events in order. Each says what changed for the files you write or for the parsers that read them.
  1. 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.

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

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

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

  5. 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, refused, and its TOML 1.0.0 spelling, read Python · forms/forms.py
"""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

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.

TOML ↔ JSON Converter In TOML 1.1.0 mode every 1.1 form of a file is listed as a note; TOML 1.0.0 mode gives the 1.0 spelling.

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

  1. Question 1 of 2 Which of these lines does tomllib on Python 3.14, a TOML 1.0.0 parser, refuse? Choose every one.

    Choose every answer that is right.

    Show the answer: Question 1 of 2

    Answer:

    • opens = 09:30
    • point = { x = 1, y = 2, }
    • sign = "Caf\xE9"

    The five 1.1 forms are line breaks and comments inside inline tables, a trailing comma in them, \e, \xHH and times without seconds. A TOML 1.0.0 parser refuses each, and its message never mentions a version.

  2. Question 2 of 2 A file must load with Python 3.14. How do you write banner = "\e[1mShelf" in TOML 1.0.0?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: banner = "\u001B[1mShelf"

    Every TOML 1.1.0 escape has a 1.0.0 spelling as a \u escape: \e is \u001B and \xHH is \u00HH.

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:

sale.toml: one sale, four kinds of date and time TOML · dates/sale.toml
# 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:

The four types in Python, and what JSON makes of them Python · dates/dates.py
"""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

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 four values, in two time zones JavaScript · dates/dates.mjs
// 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:

ids.toml: order numbers of five sizes TOML · ints/ids.toml
# 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
Large integers through tomllib Python · ints/ints.py
"""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

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:

Number() against a TOML integer reader, and BigInt in JSON JavaScript · ints/ints.mjs
// 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

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

  1. Question 1 of 2 A sale should start at the same moment for every branch, wherever its server runs. Which kind of value says that?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: An offset date-time: the day, then T10:00:00+05:30

    Use an offset date-time for an instant. Keep local date-times, dates and times for values that mean the same wall clock everywhere, such as opening hours.

  2. Question 2 of 2 A JavaScript service reads order_id = 9007199254740993 from a TOML file. What should the parser return?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The BigInt 9007199254740993n, or an error if the code only accepts numbers

    Past 9007199254740991 a JavaScript number cannot hold every integer, so a TOML reader returns a BigInt or refuses the value, as the reader of ints.mjs did in its numbers-only mode.

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, date or time: its isoformat() 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.
  • True and False stay booleans.
  • A float that is inf, -inf or nan: raise ValueError, 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.

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.

  1. Question 1 of 15 Which of these are new in TOML 1.1.0? Choose every one.

    Choose every answer that is right.

    Show the answer: Question 1 of 15

    Answer:

    • A trailing comma in an inline table
    • The \e escape in basic strings
    • A time without seconds, such as 09:30

    TOML 1.1.0 changed inline tables, added two escapes and made seconds optional; everything else is a clarification.

  2. Question 2 of 15 Does a valid TOML 1.0.0 file give the same data when a TOML 1.1.0 parser reads it?

    Choose one answer.

    Show the answer: Question 2 of 15

    Answer: Yes: TOML 1.1.0 only added forms, so every valid 1.0.0 file reads the same way

    The change is backwards compatible: new forms were added, and no old form changed its meaning.

  3. Question 3 of 15 A user of your tool on Python 3.14 reports Expected newline or end of document after a statement (at line 3, column 10), and line 3 is opens = 09:30. What is the most likely cause?

    Choose one answer.

    Show the answer: Question 3 of 15

    Answer: The file uses a TOML 1.1.0 form, a time without seconds, and Python 3.14 reads TOML 1.0.0

    A TOML 1.0.0 parser reads 09 as a value, then finds :30 where it expects the end of the line. Write 09:30:00, or have the users run Python 3.15.

  4. Question 4 of 15 What is the TOML 1.0.0 spelling of opens = 09:30?

    Choose one answer.

    Show the answer: Question 4 of 15

    Answer: opens = 09:30:00

    A time without seconds means 00 seconds, so the 1.0.0 spelling adds :00. forms.py read 09:30:00 as datetime.time(9, 30).

  5. Question 5 of 15 Which line gives the same string as sign = "Caf\xE9" and also loads in TOML 1.0.0?

    Choose one answer.

    Show the answer: Question 5 of 15

    Answer: sign = "Caf\u00E9"

    Writing é itself also works, because a TOML file is UTF-8; the \u spelling helps when the file must stay ASCII.

  6. Question 6 of 15 An inline table in a TOML 1.1.0 file has a comment on one of its lines. How do you keep the comment in TOML 1.0.0?

    Choose one answer.

    Show the answer: Question 6 of 15

    Answer: Write the table under a [header], where each key has a line and the comment has room

    forms.py rewrote the commented inline table as a [point] table with the comment on the header line.

  7. Question 7 of 15 Contributors to your project run Python 3.12 to 3.15, and a tool reads its settings from your pyproject.toml with tomllib. May the file use multi-line inline tables?

    Choose one answer.

    Show the answer: Question 7 of 15

    Answer: No: the tool runs on each contributor's Python, and tomllib before 3.15 reads TOML 1.0.0 only

    Write TOML 1.0.0 while any reader may run a TOML 1.0.0 parser, and load the file with one in CI.

  8. Question 8 of 15 What does tomllib return for the sale's reminder, a local date-time with no offset?

    Choose one answer.

    Show the answer: Question 8 of 15

    Answer: A naive datetime, with tzinfo None

    Offset date-times become aware datetimes, local date-times naive ones, local dates date and local times time.

  9. Question 9 of 15 A time in a TOML file ends in .123456789 seconds. How many microseconds does tomllib keep?

    Type a number.

    Show the answer: Question 9 of 15

    Answer: 123456 microseconds

    A Python datetime holds microseconds, six digits, and TOML says extra precision is truncated, not rounded: dates.py printed 123456, where rounding would have given 123457.

  10. Question 10 of 15 On a computer set to Indian time, what instant does new Date() hold for a date-only string, such as the sale's day?

    Choose one answer.

    Show the answer: Question 10 of 15

    Answer: Midnight UTC on that day

    MDN: date-only forms are interpreted as UTC and date-time forms as local time. A JavaScript TOML parser needs its own type for a local date for exactly this reason.

  11. Question 11 of 15 What does Python do with reminder < starts when reminder is a naive datetime and starts an aware one?

    Choose one answer.

    Show the answer: Question 11 of 15

    Answer: It raises TypeError: it will not compare offset-naive and offset-aware datetimes

    Attach the zone you mean with replace(tzinfo=…), as dates.py did with UTC+05:30, before you compare.

  12. Question 12 of 15 What number does Number("9007199254740993") give in JavaScript?

    Type a number.

    Show the answer: Question 12 of 15

    Answer: 9007199254740000

    9007199254740993 lies past the largest safe integer, 9007199254740991, and binary64 cannot hold it, so it becomes the nearest number it can hold, 9007199254740992, as ints.mjs printed.

  13. Question 13 of 15 What does tomllib do with past_int64 = 9223372036854775808, which is 2⁶³?

    Choose one answer.

    Show the answer: Question 13 of 15

    Answer: It returns the int 9223372036854775808, although a parser built on 64-bit integers must refuse it

    A file that tomllib accepts may still fail in a parser built on 64-bit integers, such as the reader of ints.mjs, which raised RangeError for it.

  14. Question 14 of 15 What does JSON.stringify({ order_id: 9007199254740993n }) do?

    Choose one answer.

    Show the answer: Question 14 of 15

    Answer: It throws TypeError: Do not know how to serialize a BigInt

    Send large integers as strings, with a replacer, and make sure the receiver expects a string.

  15. Question 15 of 15 Your JavaScript service reads a TOML file whose order ids pass 2⁵³ while every other integer is small, and the code does arithmetic on the small ones. Which smol-toml setting fits, by its README?

    Choose one answer.

    Show the answer: Question 15 of 15

    Answer: integersAsBigInt: 'asNeeded': numbers where they fit, BigInt only for the large ids

    Return BigInt where a number cannot hold the value, as the reader of ints.mjs did, and keep numbers elsewhere.

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.

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

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

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

    Model answer, Pro. First step and follow-ups

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

    Model answer, Pro. 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 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.

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

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

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

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

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

  7. 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 \e and \xHH escapes and optional seconds; a TOML 1.0.0 parser such as tomllib on 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; tomllib returns them as aware and naive datetimes, and JavaScript’s Date reads the local one differently in every time zone.
  • Precision beyond what a parser holds is truncated: tomllib keeps microseconds and a JavaScript Date keeps milliseconds.
  • tomllib keeps 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

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.