Data types¶
How TOONS maps Python values to TOON and back.
Mapping summary¶
| Python | TOON | Notes |
|---|---|---|
dict |
object | Keys must be strings; insertion order is preserved |
list, tuple |
array | Inline, tabular, or list form |
str |
string | Quoted only when needed |
int |
integer | Plain decimal, any magnitude |
float |
number | Plain decimal; NaN and infinities become null |
bool |
true / false |
Lowercase |
None |
null |
|
datetime, date, time |
string | ISO 8601, via isoformat() |
Decimal |
number | Converted through float, so trailing zeros are lost |
Decoding maps TOON back to dict, list, str, int, float, bool, and
None. A tuple therefore decodes as a list.
Strings¶
Strings are unquoted when that is unambiguous, quoted when it is not.
A string is quoted when it is empty, has a leading or trailing space or tab,
looks like a number, equals true, false, or null, starts with - or
#, or contains :, ", \, [, ], {, }, a control character, or
the active delimiter.
import toons
print(toons.dumps({"name": "Alice"}))
# name: Alice
print(toons.dumps({"text": "Hello: World"}))
# text: "Hello: World"
print(toons.dumps({"csv": "a,b,c"}))
# csv: "a,b,c"
print(toons.loads('text: "Line 1\\nLine 2"'))
# {'text': 'Line 1\nLine 2'}
Quoting is what preserves the type of a string that looks numeric:
import toons
print(toons.dumps({"zip": "12345"}))
# zip: "12345"
print(toons.loads("zip: 12345"))
# {'zip': 12345}
print(toons.loads('zip: "12345"'))
# {'zip': '12345'}
Numbers¶
TOON writes plain decimal notation, never exponents. Integers keep their exact value regardless of magnitude.
import toons
print(toons.dumps({"count": 42, "pi": 3.14, "small": 2.5e-4, "big": 1.5e10}))
# count: 42
# pi: 3.14
# small: 0.00025
# big: 15000000000
print(toons.loads("value: 1180591620717411303424"))
# {'value': 1180591620717411303424}
Floats that are not finite have no TOON representation and encode as null:
Booleans and null¶
Objects¶
Keys are written bare when they match [A-Za-z_][A-Za-z0-9_.]*, and quoted
otherwise.
import toons
print(toons.dumps({"user_id": 1, "full name": "Alice"}))
# user_id: 1
# "full name": Alice
An empty object encodes as nothing at the root, and an empty document decodes as an empty dict:
Arrays¶
Arrays always declare their length. The layout depends on the contents.
Primitive arrays are inline:
import toons
print(toons.dumps({"tags": ["python", "rust", "toon"]}))
# tags[3]: python,rust,toon
print(toons.dumps({"items": []}))
# items: []
Uniform object arrays use the tabular form, which is where TOON saves the most tokens. It applies when every element is an object with the same keys and only primitive values:
import toons
users = {
"users": [
{"name": "Alice", "age": 30, "role": "admin"},
{"name": "Bob", "age": 25, "role": "user"},
]
}
print(toons.dumps(users))
# users[2]{name,age,role}:
# Alice,30,admin
# Bob,25,user
A column whose values are uniform objects becomes a nested field group, so the rows stay flat:
import toons
orders = {
"orders": [
{"id": 1, "customer": {"name": "Ada", "country": "DK"}},
{"id": 2, "customer": {"name": "Bob", "country": "UK"}},
]
}
print(toons.dumps(orders))
# orders[2]{id,customer{name,country}}:
# 1,Ada,DK
# 2,Bob,UK
Everything else uses the list form, one - item per element:
import toons
print(toons.dumps({"items": [42, "text", {"name": "Alice"}, [1, 2, 3], None]}))
# items[5]:
# - 42
# - text
# - name: Alice
# - [3]: 1,2,3
# - null
print(toons.dumps({"users": [{"name": "Alice", "age": 30}, {"name": "Bob", "role": "admin"}]}))
# users[2]:
# - name: Alice
# age: 30
# - name: Bob
# role: admin
Arrays can be the root value:
import toons
print(toons.dumps([1, 2, 3, 4, 5]))
# [5]: 1,2,3,4,5
print(toons.dumps([{"name": "Alice", "age": 30}, {"name": "Bob", "age": 25}]))
# [2]{name,age}:
# Alice,30
# Bob,25
Objects of uniform objects¶
An object with two or more entries whose values share one uniform object shape uses the keyed tabular form, which declares the fields once and puts each entry key in front of its row:
import toons
print(toons.dumps({"a": {"x": 1, "y": 2}, "b": {"x": 3, "y": 4}}))
# [2:]{x,y}:
# a: 1,2
# b: 3,4
print(toons.dumps({"m": {"a": {"x": 1}, "b": {"x": 2}}}))
# m[2:]{x}:
# a: 1
# b: 2
Dates and times¶
datetime, date, and time objects encode as their ISO 8601 string. They
decode back as strings, not as date objects.
import toons
from datetime import date, datetime
print(toons.dumps({"day": date(2025, 2, 7)}))
# day: 2025-02-07
print(toons.dumps({"when": datetime(2025, 2, 7, 14, 30, 45)}))
# when: "2025-02-07T14:30:45"
Unsupported values¶
Values with no TOON representation raise TypeError, as they do in the
json module: sets, generators, functions, modules, file objects, and
arbitrary class instances. Object keys that are not strings also raise
TypeError.
import toons
try:
toons.dumps({"tags": {"a", "b"}})
except TypeError as exc:
print(exc)
# Object of type set is not TOON serializable
Convert such values before encoding:
import toons
class User:
def __init__(self, name, age):
self.name = name
self.age = age
def to_dict(self):
return {"name": self.name, "age": self.age}
print(toons.dumps({"user": User("Alice", 30).to_dict()}))
# user:
# name: Alice
# age: 30
A structure that references itself, directly or indirectly, raises
ValueError, and so does nesting deeper than 1000 containers.
Round trips¶
Types survive a round trip, except that tuples come back as lists and date-like objects come back as strings.
import toons
original = {
"string": "hello",
"int": 42,
"float": 3.14,
"bool": True,
"null": None,
"array": [1, 2, 3],
"object": {"nested": "value"},
}
assert toons.loads(toons.dumps(original)) == original
Documented behavior¶
The specification requires implementations to document a few choices (Sections 2, 4, 12, and 15). TOONS makes them as follows.
Numbers out of the numeric domain. Integers of any magnitude decode as
exact Python ints. A decimal or exponent token whose magnitude exceeds the
double-precision range is rejected in strict mode and decodes as
float("inf") when strict=False; a magnitude below it decodes as 0.0.
import toons
try:
toons.loads("a: 1e400")
except toons.ToonDecodeError as exc:
print(exc)
# TOON parse error at line 1: Number 1e400 is out of range
Unpaired surrogates. A str holding an unpaired surrogate has no TOON
representation and raises ValueError instead of being replaced with
U+FFFD.
Tabs in indentation. Tabs are an error in strict mode. With
strict=False a leading tab counts as one indentation level, whatever
indent_size is.
Key order and reserved keys. Object key order is preserved, except that
tabular and keyed tabular rows decode in the header's field order. No key
is special: __proto__, constructor, and prototype are ordinary dict
keys.
Nesting. Encoding or decoding more than 1000 nested containers raises an error rather than exhausting the stack.