Load Typed Settings from a TOML File

Parse TOML into frozen dataclasses, validate nested service and database tables, and report missing or mistyped keys with clear errors.

PythonConfig Management

Code

snippet
import tempfile
import tomllib
from dataclasses import dataclass
from pathlib import Path


class ConfigError(ValueError):
    pass


@dataclass(frozen=True)
class DatabaseSettings:
    host: str
    port: int
    name: str
    pool_size: int = 5


@dataclass(frozen=True)
class Settings:
    service: str
    debug: bool
    database: DatabaseSettings


def load_settings(path: Path) -> Settings:
    try:
        raw = tomllib.loads(path.read_text(encoding="utf-8"))
    except tomllib.TOMLDecodeError as error:
        raise ConfigError(f"invalid TOML: {error}") from error

    service = raw.get("service")
    if not isinstance(service, dict):
        raise ConfigError("missing [service] table")
    name = service.get("name")
    debug = service.get("debug", False)
    if not isinstance(name, str) or not name.strip():
        raise ConfigError("service.name must be a non-empty string")
    if not isinstance(debug, bool):
        raise ConfigError("service.debug must be a boolean")

    database = raw.get("database")
    if not isinstance(database, dict):
        raise ConfigError("missing [database] table")
    host = database.get("host")
    port = database.get("port")
    db_name = database.get("name")
    pool_size = database.get("pool_size", 5)
    if not isinstance(host, str) or not host:
        raise ConfigError("database.host must be a non-empty string")
    if not isinstance(port, int) or isinstance(port, bool) or not 1 <= port <= 65535:
        raise ConfigError("database.port must be an integer between 1 and 65535")
    if not isinstance(db_name, str) or not db_name:
        raise ConfigError("database.name must be a non-empty string")
    if not isinstance(pool_size, int) or isinstance(pool_size, bool) or pool_size < 1:
        raise ConfigError("database.pool_size must be a positive integer")

    return Settings(
        service=name.strip(),
        debug=debug,
        database=DatabaseSettings(
            host=host,
            port=port,
            name=db_name,
            pool_size=pool_size,
        ),
    )


def main() -> None:
    with tempfile.TemporaryDirectory() as tmp:
        path = Path(tmp) / "config.toml"
        path.write_text(
            """
[service]
name = "orders"
debug = true

[database]
host = "db.internal"
port = 5432
name = "orders"
""",
            encoding="utf-8",
        )
        settings = load_settings(path)
        print(settings)
        print("pool size:", settings.database.pool_size)

        broken = Path(tmp) / "broken.toml"
        broken.write_text(
            """
[service
name = 'x'
""",
            encoding="utf-8",
        )
        try:
            load_settings(broken)
        except ConfigError as error:
            print("error:", error)


if __name__ == "__main__":
    main()