Python tomllib
last modified October 29, 2026 Python 3.15
In this article we show how to parse TOML documents with the
tomllib module. The module has been part of the standard library
since Python 3.11; in Python 3.15 it was updated to TOML 1.1.0. The basics of
the format are covered in Python TOML; here we go
into more detail.
TOML (Tom's Obvious Minimal Language) is a configuration file
format designed to map unambiguously to a hash table. It is used by
pyproject.toml files, Cargo, and many other tools.
The tomllib module has two functions: load parses a
binary file object and loads parses a string. Both return a
dict. The module can only read TOML; to write it, a third-party
package such as tomli-w is needed.
Loading a file
The load function requires a file opened in binary mode.
title = "Inventory" version = 3 debug = false
This is a simple TOML file with three key-value pairs.
import tomllib
with open('app.toml', 'rb') as f:
config = tomllib.load(f)
print(config)
try:
with open('app.toml', encoding='utf-8') as f:
tomllib.load(f)
except TypeError as e:
print('TypeError:', e)
TOML files are always encoded in UTF-8. To make sure that the platform
encoding plays no role, load reads bytes and decodes them itself.
Passing a file opened in text mode raises a TypeError with a
helpful message.
$ python binary_mode.py
{'title': 'Inventory', 'version': 3, 'debug': False}
TypeError: File must be opened in binary mode, e.g. use `open('foo.toml', 'rb')`
Tables and dotted keys
A table is a named group of key-value pairs. Tables become nested dictionaries.
name = "shop" [database] host = "localhost" port = 5432 [database.pool] min = 2 max = 10 [logging] level = "info" file.path = "/var/log/shop.log" file.rotate = true
The [database.pool] header defines a table nested in
database. Dotted keys like file.path create nested
tables too, without a header.
import tomllib
from pprint import pprint
with open('tables.toml', 'rb') as f:
config = tomllib.load(f)
pprint(config, sort_dicts=False)
print(config['database']['pool']['max'])
print(config['logging']['file']['path'])
We print the whole structure with pprint and access two nested
values. The sort_dicts=False argument keeps the order of the
keys from the file.
$ python tables.py
{'name': 'shop',
'database': {'host': 'localhost', 'port': 5432, 'pool': {'min': 2, 'max': 10}},
'logging': {'level': 'info',
'file': {'path': '/var/log/shop.log', 'rotate': True}}}
10
/var/log/shop.log
Arrays of tables
A header in double brackets defines an element of an array of tables. Each occurrence of the header starts a new element.
[[servers]] name = "alpha" ip = "10.0.0.1" roles = ["web", "cache"] [[servers]] name = "beta" ip = "10.0.0.2" roles = ["db"] [[servers.disks]] mount = "/data" size_gb = 500 [[servers]] name = "gamma" ip = "10.0.0.3" roles = []
The file describes three servers. The [[servers.disks]] header
adds a disk to the most recently defined server, beta.
import tomllib
with open('servers.toml', 'rb') as f:
config = tomllib.load(f)
for server in config['servers']:
roles = ', '.join(server['roles']) or 'none'
print(f"{server['name']:6} {server['ip']:9} roles: {roles}")
print(config['servers'][1]['disks'])
An array of tables becomes a list of dictionaries, which we iterate over. The nested array belongs only to the second server.
$ python array_tables.py
alpha 10.0.0.1 roles: web, cache
beta 10.0.0.2 roles: db
gamma 10.0.0.3 roles: none
[{'mount': '/data', 'size_gb': 500}]
Inline tables
An inline table is written in braces and is useful for small groups of values.
origin = { x = 0, y = 0 }
window = {
title = "Editor",
size = { width = 800, height = 600 },
resizable = true,
}
TOML 1.0 requires an inline table to be written on a single line and does not
allow a trailing comma. TOML 1.1, supported since Python 3.15, allows newlines
and a trailing comma, so longer inline tables can be formatted like the
window table.
import tomllib
with open('inline.toml', 'rb') as f:
config = tomllib.load(f)
print(config['origin'])
print(config['window'])
print(config['window']['size']['width'])
Inline tables are converted to dictionaries in the same way as tables with headers.
$ python inline.py
{'x': 0, 'y': 0}
{'title': 'Editor', 'size': {'width': 800, 'height': 600}, 'resizable': True}
800
Strings
TOML has four kinds of strings: basic and literal strings, each in a single line and a multiline form.
basic = "Tab:\tend, quote: \"x\", unicode: \u00e1"
literal = 'C:\Users\john\docs'
multiline = """
Roses are red,
Violets are blue."""
folded = """\
The quick brown \
fox jumps."""
regex = '''\d{3}-\d{4}'''
hex = "\x41\x42\x43"
escape = "\e[1mbold\e[0m"
Basic strings in double quotes support escape sequences. Literal strings in single quotes take the text as it is, which suits Windows paths and regular expressions.
folded = """\
The quick brown \
fox jumps."""
In a multiline basic string, a backslash at the end of a line removes the newline and the whitespace that follows it. A newline directly after the opening quotes is dropped.
hex = "\x41\x42\x43" escape = "\e[1mbold\e[0m"
The \xHH escape for a character code and the \e
escape for the escape character are new in TOML 1.1.
import tomllib
with open('strings.toml', 'rb') as f:
data = tomllib.load(f)
for key, value in data.items():
print(f'{key:10} {value!r}')
We print the representation of each value to show the escapes clearly.
$ python strings.py
basic 'Tab:\tend, quote: "x", unicode: รก'
literal 'C:\\Users\\john\\docs'
multiline 'Roses are red,\nViolets are blue.'
folded 'The quick brown fox jumps.'
regex '\\d{3}-\\d{4}'
hex 'ABC'
escape '\x1b[1mbold\x1b[0m'
Dates and times
TOML has four date and time types, which map to objects of the
datetime module.
released = 2026-10-01T09:00:00+02:00 utc = 2026-10-01T07:00:00Z local = 2026-10-29T14:30:00 day = 2026-10-29 alarm = 06:45:30 meeting = 10:30
An offset date-time contains a time zone offset or Z for UTC. A
local date-time, a local date, and a local time have no time zone. TOML 1.1
makes the seconds optional, as in meeting = 10:30.
import tomllib
with open('dates.toml', 'rb') as f:
data = tomllib.load(f)
for key, value in data.items():
print(f'{key:9} {type(value).__name__:9} {value!r}')
print(data['released'] == data['utc'])
Offset date-times become aware datetime objects with a
tzinfo; local date-times are naive.
print(data['released'] == data['utc'])
The two offset date-times describe the same instant, so they compare equal.
$ python dates.py released datetime datetime.datetime(2026, 10, 1, 9, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))) utc datetime datetime.datetime(2026, 10, 1, 7, 0, tzinfo=datetime.timezone.utc) local datetime datetime.datetime(2026, 10, 29, 14, 30) day date datetime.date(2026, 10, 29) alarm time datetime.time(6, 45, 30) meeting time datetime.time(10, 30) True
Numbers
Integers can be written with underscores and in hexadecimal, octal, or binary
notation. Floats include the special values inf and
nan.
count = 1_000_000 hex = 0xff octal = 0o755 binary = 0b1010 negative = -42 ratio = 0.1 avogadro = 6.022e23 limit = inf missing = nan price = 19.99
The file contains different notations of integers and floats.
import tomllib
from decimal import Decimal
with open('numbers.toml', 'rb') as f:
data = tomllib.load(f)
print(data)
with open('numbers.toml', 'rb') as f:
exact = tomllib.load(f, parse_float=Decimal)
print(exact['price'], exact['ratio'] * 3, data['ratio'] * 3)
print(exact['limit'], exact['missing'])
By default, floats are parsed into Python float objects.
exact = tomllib.load(f, parse_float=Decimal)
The parse_float parameter takes a function that is called with
the text of each float. With Decimal, prices and other decimal
values are represented exactly; three times 0.1 is then exactly
0.3. The function also receives the special values, which
Decimal understands as well.
$ python read_numbers.py
{'count': 1000000, 'hex': 255, 'octal': 493, 'binary': 10, 'negative': -42, 'ratio': 0.1, 'avogadro': 6.022e+23, 'limit': inf, 'missing': nan, 'price': 19.99}
19.99 0.3 0.30000000000000004
Infinity NaN
Parse errors
Invalid documents raise tomllib.TOMLDecodeError, a subclass of
ValueError.
import tomllib
documents = [
'name = "shop"\nport = \n',
'port = 80\nport = 8080\n',
'[server]\nhost = "a"\n[server]\nport = 80\n',
'name = shop\n',
'title = "unterminated\n',
]
for doc in documents:
try:
tomllib.loads(doc)
except tomllib.TOMLDecodeError as e:
print(f'line {e.lineno}, column {e.colno}: {e.msg}')
We parse five invalid documents: a missing value, a duplicate key, a table defined twice, an unquoted string, and an unterminated string.
print(f'line {e.lineno}, column {e.colno}: {e.msg}')
Since Python 3.14, the exception has the msg, doc,
pos, lineno, and colno attributes, so
programs can report the error position in their own format. TOML forbids
defining a key or a table twice, which catches many copy-and-paste mistakes in
configuration files.
$ python errors.py
line 2, column 8: Invalid value
line 2, column 12: Cannot overwrite a value
line 3, column 8: Cannot declare ('server',) twice
line 1, column 8: Invalid value
line 1, column 22: Illegal character '\n'
TOML 1.1 support
The next program tests the TOML 1.1 features one by one.
import sys
import tomllib
snippets = {
'multiline inline table': 'p = {\n x = 1,\n y = 2,\n}',
'trailing comma': 'p = { x = 1, y = 2, }',
'\\x escape': 'a = "\\x41"',
'\\e escape': 'e = "\\e[1m"',
'time without seconds': 't = 10:30',
}
print('Python', sys.version.split()[0])
for feature, doc in snippets.items():
try:
result = tomllib.loads(doc)
print(f'{feature:24} ok {result}')
except tomllib.TOMLDecodeError as e:
print(f'{feature:24} error {e.msg}')
Each snippet uses one feature that TOML 1.0 does not allow. In Python 3.15, all of them are accepted. The update is backwards compatible: every valid TOML 1.0 document is parsed as before.
$ python compat.py
Python 3.15.2
multiline inline table ok {'p': {'x': 1, 'y': 2}}
trailing comma ok {'p': {'x': 1, 'y': 2}}
\x escape ok {'a': 'A'}
\e escape ok {'e': '\x1b[1m'}
time without seconds ok {'t': datetime.time(10, 30)}
Older Python versions
Python 3.14 and older versions of tomllib implement TOML 1.0, so
they reject all five snippets. Configuration files that must be read by
older Python versions or by other TOML 1.0 tools should avoid the new syntax.
$ python3.14 compat.py Python 3.14.0 multiline inline table error Invalid initial character for a key part trailing comma error Invalid initial character for a key part \x escape error Unescaped '\' in a string \e escape error Unescaped '\' in a string time without seconds error Expected newline or end of document after a statement
Reading pyproject.toml
The most common TOML file in the Python world is pyproject.toml,
which describes a project.
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "inventory"
version = "1.4.0"
requires-python = ">=3.12"
dependencies = [
"httpx>=0.28",
"rich>=14.0",
]
[project.optional-dependencies]
dev = ["pytest>=8.4", "ruff"]
[project.scripts]
inventory = "inventory.cli:main"
[tool.ruff]
line-length = 88
The [project] table contains the standard metadata. Tools keep
their own settings in subtables of [tool].
import tomllib
from pathlib import Path
data = tomllib.loads(Path('pyproject.toml').read_text(encoding='utf-8'))
project = data['project']
print(f"{project['name']} {project['version']} (Python {project['requires-python']})")
print('dependencies:', ', '.join(project['dependencies']))
for extra, deps in project.get('optional-dependencies', {}).items():
print(f'extra {extra}:', ', '.join(deps))
ruff = data.get('tool', {}).get('ruff', {})
print('line length:', ruff.get('line-length', 'default'))
This time we read the file into a string and parse it with
loads. Optional tables are read with get and empty
dictionaries as defaults, so the program also works for projects without
optional dependencies or tool settings.
$ python pyproject_info.py inventory 1.4.0 (Python >=3.12) dependencies: httpx>=0.28, rich>=14.0 extra dev: pytest>=8.4, ruff line length: 88
Loading settings into dataclasses
The tomllib module does not check the structure of the data. A
common approach is to load the dictionaries into dataclasses, which provide
defaults and validation.
[server] host = "0.0.0.0" port = 8080 [database] url = "postgresql://localhost/shop" timeout = 2.5
The settings file sets only some of the values.
import tomllib
from dataclasses import dataclass, field
@dataclass
class Server:
host: str = '127.0.0.1'
port: int = 8000
workers: int = 2
def __post_init__(self):
if not 0 < self.port < 65536:
raise ValueError(f'invalid port: {self.port}')
@dataclass
class Database:
url: str
timeout: float = 5.0
@dataclass
class Settings:
server: Server = field(default_factory=Server)
database: Database | None = None
def load_settings(path):
with open(path, 'rb') as f:
data = tomllib.load(f)
db = data.get('database')
return Settings(
server=Server(**data.get('server', {})),
database=Database(**db) if db else None,
)
settings = load_settings('settings.toml')
print(settings.server)
print(settings.database)
try:
Server(**tomllib.loads('port = 70000'))
except ValueError as e:
print('ValueError:', e)
try:
Server(**tomllib.loads('hostname = "x"'))
except TypeError as e:
print('TypeError:', e)
Each table is passed as keyword arguments to a dataclass.
return Settings(
server=Server(**data.get('server', {})),
database=Database(**db) if db else None,
)
The missing workers value gets its default. Keys that the
dataclass does not know raise a TypeError, so misspelled
settings are reported instead of being silently ignored.
def __post_init__(self):
if not 0 < self.port < 65536:
raise ValueError(f'invalid port: {self.port}')
The __post_init__ method checks the values. For larger
configurations, validation libraries such as Pydantic can do the same with
less code.
$ python settings.py Server(host='0.0.0.0', port=8080, workers=2) Database(url='postgresql://localhost/shop', timeout=2.5) ValueError: invalid port: 70000 TypeError: Server.__init__() got an unexpected keyword argument 'hostname'
Source
In this article we have parsed TOML documents with the Python
tomllib module.
Author
List all Python tutorials.