Python 3.11 added tomllib, giving applications a standard-library parser for TOML configuration files. That removes a dependency for a common task, but parsing is only one part of loading configuration correctly.

A configuration loader still needs to decide how large an input may be, what keys and types are accepted, whether floating-point values require exact decimal semantics, and how syntax errors should be reported. It also needs to remember that tomllib reads TOML; it is not a TOML writer or a schema validator.

This article builds a small configuration boundary around those responsibilities.

Parse files in binary mode

For a TOML file, tomllib.load() expects a readable binary file object:

import tomllib

with open("service.toml", "rb") as file:
    config = tomllib.load(file)

The result is a normal Python dictionary containing recursively converted TOML values.

Opening the file as "rb" is deliberate. Do not open it in text mode merely because TOML is a textual format and then pass that stream to tomllib.load().

When the TOML document is already available as a Python string, use tomllib.loads() instead:

import tomllib

text = """
[server]
host = "127.0.0.1"
port = 8080
"""

config = tomllib.loads(text)

The distinction makes ownership clear: load() consumes a binary file-like object, while loads() parses a str already held by the application.

Know what types come back

TOML types are converted into useful Python types. Strings become str, integers become int, booleans become bool, arrays become lists, and tables become dictionaries.

TOML date and time values are more interesting because they become objects from datetime rather than remaining strings. For example:

release_date = 2026-09-08
maintenance = 02:30:00
started_at = 2026-09-08T01:15:00Z

After parsing, these values are represented by appropriate datetime.date, datetime.time, and datetime.datetime objects. An offset date-time has timezone information, while a local date-time does not.

That is convenient, but it means downstream validation should check the Python types produced by the parser rather than assuming every configuration value is textual.

Parsing is not validation

A syntactically valid document can still be invalid for your application.

Consider this TOML:

[server]
host = "127.0.0.1"
port = -1
workers = 0

The document is valid TOML. The values may still violate the service’s requirements.

Keep parsing and application validation as separate steps:

from dataclasses import dataclass
import tomllib


@dataclass(frozen=True)
class ServerConfig:
    host: str
    port: int
    workers: int


def parse_server_config(data: dict) -> ServerConfig:
    server = data.get("server")
    if not isinstance(server, dict):
        raise ValueError("server must be a table")

    allowed = {"host", "port", "workers"}
    unknown = server.keys() - allowed
    if unknown:
        names = ", ".join(sorted(unknown))
        raise ValueError(f"unknown server keys: {names}")

    host = server.get("host")
    port = server.get("port")
    workers = server.get("workers")

    if not isinstance(host, str) or not host:
        raise ValueError("server.host must be a non-empty string")

    # bool is a subclass of int, so reject it explicitly.
    if isinstance(port, bool) or not isinstance(port, int):
        raise ValueError("server.port must be an integer")
    if not 1 <= port <= 65535:
        raise ValueError("server.port must be between 1 and 65535")

    if isinstance(workers, bool) or not isinstance(workers, int):
        raise ValueError("server.workers must be an integer")
    if workers < 1:
        raise ValueError("server.workers must be positive")

    return ServerConfig(host=host, port=port, workers=workers)


def load_config(path: str) -> ServerConfig:
    with open(path, "rb") as file:
        raw = tomllib.load(file)
    return parse_server_config(raw)

This boundary gives the rest of the program a stronger guarantee. Code receiving ServerConfig does not need to repeatedly ask whether port exists or whether workers is positive.

Rejecting unknown keys is also useful for configuration files. A misspelled worker key should usually fail startup rather than silently leave workers at some unrelated default.

Be careful when checking integer types

Python has one subtle validation trap: bool is a subclass of int.

isinstance(True, int)  # True

If a setting requires an integer and booleans should not be accepted, a bare isinstance(value, int) check is too permissive. Explicitly exclude bool, as the previous example does.

This issue is not specific to TOML. It appears whenever dynamically typed data is converted into a stricter application model.

Use parse_float when binary floats are the wrong representation

By default, TOML floating-point values become Python float objects:

price = 19.99

For measurements where normal floating-point behavior is acceptable, that default is usually appropriate. For decimal quantities such as prices, you may want decimal.Decimal instead.

tomllib exposes that choice through parse_float:

from decimal import Decimal
import tomllib

with open("pricing.toml", "rb") as file:
    config = tomllib.load(file, parse_float=Decimal)

price = config["price"]
assert isinstance(price, Decimal)

The parser calls the supplied function with the textual representation of each TOML float. This lets the application choose a numeric representation at the parsing boundary instead of first converting the value to a binary float and trying to recover decimal intent afterward.

The callable used for parse_float must not return a dictionary or list. Keep it focused on producing a scalar representation for a TOML floating-point value.

Handle syntax errors separately from application errors

Invalid TOML raises tomllib.TOMLDecodeError. That is different from a document that parses correctly but fails your schema or business rules.

Keeping those cases separate makes diagnostics clearer:

import tomllib


def read_raw_config(path: str) -> dict:
    try:
        with open(path, "rb") as file:
            return tomllib.load(file)
    except tomllib.TOMLDecodeError as exc:
        raise RuntimeError(f"invalid TOML in {path}: {exc}") from exc

File errors such as FileNotFoundError and PermissionError are different again. Whether to wrap them depends on the interface your application wants to expose, but avoid turning every failure into the same generic message. Operators benefit from knowing whether a file is missing, unreadable, syntactically malformed, or semantically invalid.

Newer Python releases expose structured position information on TOMLDecodeError, but code intended to support older tomllib versions should not assume those newer attributes exist. The exception’s string representation remains a portable way to include parser diagnostics.

Bound untrusted configuration before parsing it

Configuration is often trusted local input, but that is not always true. A service might accept uploaded configuration, receive it from a control plane, or parse files from a tenant-controlled workspace.

Do not treat a parser as a resource sandbox. Python’s documentation recommends limiting the size of untrusted TOML because malicious input can consume substantial CPU or memory.

If you already have bytes from an external source, enforce the byte limit before decoding and parsing:

import tomllib

MAX_CONFIG_BYTES = 256 * 1024


def parse_uploaded_toml(data: bytes) -> dict:
    if len(data) > MAX_CONFIG_BYTES:
        raise ValueError("configuration is too large")

    try:
        text = data.decode("utf-8")
    except UnicodeDecodeError as exc:
        raise ValueError("configuration is not valid UTF-8") from exc

    return tomllib.loads(text)

For a stream, do not call an unbounded read() and check the size afterward. At that point the application has already accepted the memory cost. Read at most one byte beyond the permitted limit:

import tomllib

MAX_CONFIG_BYTES = 256 * 1024


def parse_limited_stream(stream) -> dict:
    data = stream.read(MAX_CONFIG_BYTES + 1)
    if len(data) > MAX_CONFIG_BYTES:
        raise ValueError("configuration is too large")

    try:
        text = data.decode("utf-8")
    except UnicodeDecodeError as exc:
        raise ValueError("configuration is not valid UTF-8") from exc

    return tomllib.loads(text)

This example assumes stream.read(n) honors the conventional bounded-read contract. For network-facing systems, transport-level request limits are also valuable because they reject oversized bodies before application parsing begins.

A byte limit is not a complete CPU or memory guarantee, but it creates an important outer bound and prevents trivially enormous documents from reaching the parser.

Do not use tomllib to edit a TOML file

tomllib is intentionally a parser, not a writer.

That matters for tools that need to modify configuration. Loading a document into a dictionary loses the source-level representation needed for tasks such as preserving comments, spacing, and the author’s formatting choices. Even if you later serialize the dictionary with some other mechanism, that is not equivalent to editing the original TOML document faithfully.

Use tomllib when the application needs to consume configuration. Choose a TOML writing or style-preserving library when the application needs to generate or edit TOML.

This separation also helps keep server configuration loaders small: reading, validation, and conversion can remain independent from configuration-authoring concerns.

Validate before applying side effects

A useful configuration loader should finish validation before changing process state.

Avoid a flow like this:

raw = load_toml()
start_listener(raw["server"]["port"])
validate_database(raw["database"])

If database validation fails, the process has already opened a listener. More complicated reload systems can leave partially applied state behind.

Prefer building a complete validated model first:

raw = load_toml()
config = validate_config(raw)
apply_config(config)

For live reloads, go further: parse and validate the candidate configuration independently, then perform a deliberate transition from the old valid state to the new one. Parsing success alone should never trigger incremental side effects.

Test the boundary, not only the happy path

Configuration tests should include documents that are valid TOML but invalid for the application.

Useful cases include:

  • a missing required table;
  • an unknown or misspelled key;
  • a value with the wrong TOML type;
  • an integer outside its permitted range;
  • a boolean supplied where an integer is expected;
  • malformed TOML;
  • an input one byte over the configured size limit;
  • decimal values when a custom parse_float is required.

Also test defaults deliberately. A missing key and a key explicitly set to an empty or zero value may need different meanings. Using dict.get() without thinking about that distinction can accidentally collapse those states.

Keep the parsing boundary narrow

tomllib solves a precise problem: converting TOML into Python values. A production configuration boundary usually needs a little more structure around it.

Open TOML files in binary mode for tomllib.load(). Use tomllib.loads() for strings. Decide whether normal float values are appropriate or whether parse_float should produce another numeric type. Catch syntax failures distinctly, validate keys and value ranges after parsing, and limit untrusted input before handing it to the parser.

Most importantly, do not let a dictionary returned by the parser become an implicit schema for the whole application. Convert parsed data into a validated application model first. That keeps TOML syntax concerns at the edge and gives the rest of the program configuration it can safely rely on.