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
trueas a port.boolis a subclass ofint, soisinstance(True, int)isTrue. Rejectboolbefore you check forint, asread_portdoes. - Using
typing.castto validate.cast(int, value)tells mypy to treat the value as anint. 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 Hide 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
linenoandcolnoattributes toTOMLDecodeError, 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.