Skip to content
← All Python articles

Python explainer

Read and Check a TOML Config File

About 3 min to read. Allow extra time to work through examples.

Read a TOML file with the standard library, check each setting's type and range yourself, and report the setting to fix when one is wrong.

This article uses Python. You can make it your practice mode to browse related content across the site.

Your service reads its settings from a file called config.toml. Since Python 3.11, the standard library can read TOML with the tomllib module, so there is no package to install.

import tomllib

text = '''
[server]
host = "localhost"
port = 8080
'''
config = tomllib.loads(text)
print(config)  # {'server': {'host': 'localhost', 'port': 8080}}

tomllib.loads reads a string. To read a file, open it in binary mode with open(path, "rb") and pass the file to tomllib.load, which decodes the UTF-8 itself. A document that is not valid TOML raises tomllib.TOMLDecodeError. The module only reads TOML. It cannot write it.

A section such as [server] is a table, and it becomes a dict.

Valid TOML is not a valid config

port = 8080 and port = "8080" are both valid TOML. Only the first is a port number your server can use. The parser checks the syntax. Checking which settings are there, and what types they have, is your job.

mypy cannot help on its own here. tomllib.loads returns dict[str, Any], and Any switches type checking off for a value. mypy lets you pass an Any where an int should go, even when the value is a string at runtime.

The fix is to give each value the type object and check it with isinstance. object is the opposite of Any. mypy rejects every use of the value until you check its type.

def read_port(text: str) -> int:
    document = tomllib.loads(text)
    server: object = document.get("server")
    if not isinstance(server, dict):
        raise ValueError("server: expected a table")
    port: object = server.get("port")
    if isinstance(port, bool) or not isinstance(port, int):
        raise ValueError("server.port: expected an integer")
    if not 1 <= port <= 65535:
        raise ValueError("server.port: out of range")
    return port


print(read_port('[server]\nport = 8080'))  # 8080

There are two common mistakes:

  • Accepting true as a port. bool is a subclass of int, so isinstance(True, int) is True. Reject bool before you check for int, as read_port does.
  • Using typing.cast to validate. cast(int, value) tells mypy to treat the value as an int. At runtime it returns the value unchanged and checks nothing.

Return a typed object and a clear error

Once every value has passed its check, build a frozen dataclass from them. The rest of your code then reads config.port, and mypy knows it is an int. A dataclass does not check types when you create it, so the checks must come first.

When a check fails, raise an error that names the setting and the reason, such as ConfigError("server.port", "out of range"). Then the person who edits the file knows which line to fix.

Check your understanding Can a default hide an invalid setting? Show answer

Yes, if you write it the wrong way. server.get("workers", 4) uses 4 only when the key is missing, so a present workers = 0 still reaches your check. server.get("workers") or 4 also replaces 0, because 0 is falsy, so an invalid zero quietly becomes the default. The exercise uses the default only when the key is missing, and it rejects workers = 0. TOML has no null value, so a key is either present with a value or missing.

Going further

  • Python 3.14 added lineno and colno attributes to TOMLDecodeError, so you can point at the line and column that failed.
  • Python 3.15 reads TOML 1.1, which allows, for example, line breaks and a trailing comma inside an inline table. Earlier versions read TOML 1.0 only. If the same file must load on older versions, use only TOML 1.0.
  • To write TOML, use a third-party package such as tomli-w.
  • See the tomllib documentation.

step 52 of 55

Everyday Standard Library Tools

Up next Load a Server Config and Report the First Bad Setting