TOML is a configuration format where every document maps to one hash table of keys and typed values, and it is what pyproject.toml and Cargo.toml files are written in. This sheet is for developers who read or write TOML config by hand. The confusion it clears up: [table] and [[array of tables]] look alike but build different structures, and a key can be defined only once across both dotted keys and headers. Code below was parsed with Python 3.11 tomllib, which implements TOML 1.0.0.
| Rule | Detail |
|---|---|
| File encoding | UTF-8 only |
| Case | Keys and values are case-sensitive, so `true` works and `True` is an error |
| Whitespace | Tab or space, ignored around keys, values and `=` |
| Line ending | LF or CRLF |
| Comment | `#` to end of line, except inside a string |
| One pair per line | `a = 1 b = 2` is an error |
| File extension | `.toml` |
| MIME type | `application/toml` |
| Style | Example | Notes |
|---|---|---|
| Bare | `site_name = "x"` | Only `A-Za-z0-9_-`, and digits-only keys like `1234` are strings |
| Quoted | `"127.0.0.1" = "x"` | Basic or literal string quotes allow any characters |
| Dotted | `physical.color = "orange"` | Creates a table `physical` with key `color` |
| Empty bare | `= "x"` | Invalid |
| Empty quoted | `"" = "x"` | Valid but discouraged |
| Type | Example | Python type from `tomllib` |
|---|---|---|
| String | `"text"`, `'text'`, `"""text"""`, `'''text'''` | `str` |
| Integer | `42`, `+99`, `-17`, `1_000` | `int` |
| Hex, octal, binary | `0xDEADBEEF`, `0o755`, `0b1101` | `int` |
| Float | `3.1415`, `5e+22`, `-0.01` | `float` |
| Special floats | `inf`, `-inf`, `nan` | `float` |
| Boolean | `true`, `false` | `bool` |
| Offset date-time | `2026-10-05T09:30:00+02:00` | `datetime` with a zone |
| Local date-time | `2026-10-05T09:30:00` | `datetime` without a zone |
| Local date | `2026-10-05` | `date` |
| Local time | `09:30:00` | `time` |
| Array | `[1, 2, 3]` | `list` |
| Inline table | `{ x = 1, y = 2 }` | `dict` |
There is no null type. A key with no value, key =, is invalid, so omit the key instead.
| Kind | Delimiter | Escapes | Newlines |
|---|---|---|---|
| Basic | `"..."` | Yes | No |
| Multi-line basic | `"""..."""` | Yes | Yes, and the first newline is trimmed |
| Literal | `'...'` | None | No |
| Multi-line literal | `'''...'''` | None | Yes, and the first newline is trimmed |
Escapes in basic strings are \b, \t, \n, \f, \r, \", \\, \uXXXX and \UXXXXXXXX. Any other backslash sequence is an error. A line-ending backslash inside a multi-line basic string removes the newline and the whitespace after it.
| Rule | Valid | Invalid |
|---|---|---|
| Underscores need a digit on each side | `1_000`, `224_617.445_991_228` | `_1`, `1_` |
| No leading zeros in decimal integers | `0`, `-0`, `+0` | `007` |
| Prefixed forms allow leading zeros | `0o01234567` | `+0xFF` |
| Decimal point needs digits on both sides | `0.7`, `7.0` | `.7`, `7.`, `3.e+20` |
| Special floats are lowercase | `inf`, `nan` | `Inf`, `NaN` |
| Integer range | -2^63 to 2^63-1 should work | A parser must error if it cannot hold the value |
| Syntax | Meaning |
|---|---|
| `[server]` | Table named `server`, up to the next header |
| `[a.b.c]` | Nested tables, parent tables are created implicitly |
| `[ a . b ]` | Same as `[a.b]`, spaces are ignored |
| `[[products]]` | Append a new table to the array `products` |
| `[products.dims]` | Sub-table of the most recent `[[products]]` element |
| `a = { x = 1 }` | Inline table, fully closed, nothing can be added later |
| Feature | 1.0.0 (2021-01-11) | 1.1.0 (2025-12-18) |
|---|---|---|
| Inline table over several lines | Not allowed | Allowed |
| Trailing comma in an inline table | Not allowed | Allowed |
| Seconds in a time, `14:15` | Required | Optional |
| `\xHH` escape for code points up to 255 | No | Yes |
| `\e` escape for the escape character | No | Yes |
import tomllib
cfg = tomllib.loads('''
title = "ZipKit"
port = 8_080
mask = 0o755
ratio = 6.626e-34
debug = false
tags = ["a", 1, 2.5]
[database]
url = "postgres://localhost/app"
''')
print(cfg)
print(cfg["port"] + 1, type(cfg["mask"]).__name__, cfg["mask"])
{'title': 'ZipKit', 'port': 8080, 'mask': 493, 'ratio': 6.626e-34, 'debug': False, 'tags': ['a', 1, 2.5], 'database': {'url': 'postgres://localhost/app'}}
8081 int 493
Keys before the first header belong to the root table. Octal 0o755 is the integer 493, which is handy for Unix file modes. Arrays can mix types in 1.0.
import tomllib
dotted = tomllib.loads('''
server.host = "localhost"
server.port = 8080
''')
header = tomllib.loads('''
[server]
host = "localhost"
port = 8080
''')
inline = tomllib.loads('server = { host = "localhost", port = 8080 }')
print(dotted)
print(dotted == header == inline)
{'server': {'host': 'localhost', 'port': 8080}}
True
All three forms produce the same dictionary. Use dotted keys for one or two nested values, headers for a group of settings and inline tables for short records inside arrays.
import tomllib
doc = '''
[[products]]
name = "Hammer"
sku = 738594937
[[products]]
[[products]]
name = "Nail"
sku = 284758393
[products.dims]
mm = 40
'''
for p in tomllib.loads(doc)["products"]:
print(p)
{'name': 'Hammer', 'sku': 738594937}
{}
{'name': 'Nail', 'sku': 284758393, 'dims': {'mm': 40}}
Each [[products]] header starts a new element, and a later [products.dims] attaches to the most recent one. The empty second header creates an empty table.
import tomllib
doc = '''
released = 2026-10-05
at = 2026-10-05T09:30:00+02:00
utc = 2026-10-05T07:30:00Z
local = 2026-10-05T09:30:00
clock = 09:30:00
'''
for k, v in tomllib.loads(doc).items():
print(k, type(v).__name__, repr(v))
released date datetime.date(2026, 10, 5)
at datetime datetime.datetime(2026, 10, 5, 9, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200)))
utc datetime datetime.datetime(2026, 10, 5, 7, 30, tzinfo=datetime.timezone.utc)
local datetime datetime.datetime(2026, 10, 5, 9, 30)
clock time datetime.time(9, 30)
Dates are real types, not strings, and they follow RFC 3339. A date-time without an offset is a local value that cannot be turned into an instant without outside information. A space may replace the T.
import tomllib
doc = '''basic = "tab\\there \\u00e9"
literal = 'C:\\Users\\app'
multi = """
Roses are red
Violets are blue"""
folded = """\\
The quick brown \\
fox jumps."""
quote = """She said "hi"."""
'''
print(tomllib.loads(doc))
{'basic': 'tab\there é', 'literal': 'C:\\Users\\app', 'multi': 'Roses are red\nViolets are blue', 'folded': 'The quick brown fox jumps.', 'quote': 'She said "hi".'}
The Python source doubles the backslashes, so the TOML text itself has single ones. Use literal strings with single quotes for Windows paths and regular expressions, because nothing inside them is escaped.
import tomllib
with open("pp.toml", "rb") as f:
print(tomllib.load(f)["project"]["name"])
demo
Open the file in binary mode. Passing a text-mode file raises a TypeError that says the file must be opened in binary mode. tomllib only reads TOML and has no writer, so use a third-party package to write it.
import tomllib
for bad in ['[fruit]\napple = "red"\n[fruit]\n', 'a = 1\na = 2\n', 'n = 007\n', 'name = Tom\n']:
try:
tomllib.loads(bad)
except tomllib.TOMLDecodeError as e:
print(repr(bad), '->', e)
'[fruit]\napple = "red"\n[fruit]\n' -> Cannot declare ('fruit',) twice (at line 3, column 7)
'a = 1\na = 2\n' -> Cannot overwrite a value (at line 2, column 6)
'n = 007\n' -> Expected newline or end of document after a statement (at line 1, column 6)
'name = Tom\n' -> Invalid value (at line 1, column 8)
Catch TOMLDecodeError at startup and print the message, which includes the line and column. Failing fast on a bad config is better than running with defaults. The messages are from Python 3.11, and other parsers word them differently.
[fruit] followed later by another [fruit] fails with Cannot declare ('fruit',) twice (at line 3, column 7). Merge the keys under one header.a = 1 then a = 2 fails with Cannot overwrite a value (at line 2, column 6). PyYAML keeps the last value silently, but TOML rejects it.n = 007 and name = Tom both fail. Write n = 7 and name = "Tom", because every string value needs quotes.null is parsed as an invalid value. Leave the key out and let the application supply a default.type = { name = "Nail" }, the line type.edible = false fails with Cannot mutate immutable namespace. Put everything inside the braces or use a normal table.fruits = [] followed by [[fruits]] is an error, because [[...]] only extends arrays of tables.t = 14:15, \e and multi-line inline tables fail in Python 3.11 tomllib, which implements TOML 1.0.0. Check the spec version your parser supports before you use newer syntax.a.b = 2 makes table a, while "a.b" = 1 makes one key named a.b. Quote any key that really contains a dot, such as a domain name.p = {x = 1, y = 2,} is invalid in 1.0 and errors with Invalid initial character for a key part.