ZetCode

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.

app.toml
title = "Inventory"
version = 3
debug = false

This is a simple TOML file with three key-value pairs.

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

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

tables.py
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.toml
[[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.

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

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

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

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

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

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

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

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

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

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

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

pyproject.toml
[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].

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

settings.toml
[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.

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

tomllib – Parse TOML files

TOML v1.1.0 specification

In this article we have parsed TOML documents with the Python tomllib module.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Python tutorials.