Python TOML Configuration — Deep Dive
TOML’s design philosophy — obvious, minimal, typed — makes it ideal for configuration files. This deep dive covers the TOML specification’s nuances, advanced parsing techniques, validation patterns, and production-grade configuration architectures built on TOML.
TOML Specification Deep Dive
String Types
TOML has four string types:
# Basic string (escape sequences processed)
basic = "Hello\nWorld"
# Multi-line basic string
multiline = """
Line one
Line two
"""
# Literal string (no escape processing)
literal = 'C:\Users\path\to\file'
# Multi-line literal string
regex = '''
\d+\.\d+\.\d+
'''
The literal string types are particularly useful for Windows paths and regex patterns where backslash-heavy content would need excessive escaping.
Integer Formats
decimal = 1_000_000 # Underscores for readability
hex = 0xDEADBEEF
octal = 0o755
binary = 0b11010110
positive_inf = inf
negative_inf = -inf
not_a_number = nan
Python parses these into native int and float types.
Date and Time Types
TOML has first-class datetime support:
# Offset date-time (maps to datetime.datetime with tzinfo)
odt = 2026-03-28T10:00:00+02:00
# Local date-time (maps to datetime.datetime without tzinfo)
ldt = 2026-03-28T10:00:00
# Local date (maps to datetime.date)
ld = 2026-03-28
# Local time (maps to datetime.time)
lt = 10:00:00
import tomllib
config = tomllib.loads('created = 2026-03-28T10:00:00+02:00')
print(type(config["created"])) # <class 'datetime.datetime'>
print(config["created"].tzinfo) # UTC+02:00
Array of Tables: Advanced Patterns
# Nested arrays of tables
[[fruits]]
name = "apple"
[[fruits.varieties]]
name = "red delicious"
[[fruits.varieties]]
name = "granny smith"
[[fruits]]
name = "banana"
[[fruits.varieties]]
name = "plantain"
# Parses to:
{
"fruits": [
{
"name": "apple",
"varieties": [
{"name": "red delicious"},
{"name": "granny smith"},
],
},
{
"name": "banana",
"varieties": [
{"name": "plantain"},
],
},
]
}
Validation with Pydantic
Type-Safe Configuration
import tomllib
from pydantic import BaseModel, Field, field_validator
from pathlib import Path
class DatabaseConfig(BaseModel):
host: str = "localhost"
port: int = Field(default=5432, ge=1, le=65535)
name: str
pool_size: int = Field(default=10, ge=1, le=100)
ssl: bool = False
class LoggingConfig(BaseModel):
level: str = "INFO"
format: str = "%(asctime)s %(levelname)s %(message)s"
file: Path | None = None
@field_validator("level")
@classmethod
def validate_level(cls, v):
allowed = {"DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"}
if v.upper() not in allowed:
raise ValueError(f"level must be one of {allowed}")
return v.upper()
class AppConfig(BaseModel):
debug: bool = False
secret_key: str
database: DatabaseConfig
logging: LoggingConfig = LoggingConfig()
allowed_origins: list[str] = ["http://localhost:3000"]
def load_config(path: str = "config.toml") -> AppConfig:
with open(path, "rb") as f:
raw = tomllib.load(f)
return AppConfig(**raw)
Configuration with Environment Overrides
import tomllib
import os
from pydantic import BaseModel
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
"""Load from TOML, override with environment variables."""
debug: bool = False
database_host: str = "localhost"
database_port: int = 5432
database_name: str = "myapp"
secret_key: str = ""
model_config = {
"env_prefix": "APP_", # APP_DATABASE_HOST overrides database_host
}
def load_settings(config_path: str = "config.toml") -> Settings:
"""Load TOML defaults, then apply environment overrides."""
with open(config_path, "rb") as f:
toml_config = tomllib.load(f)
# Flatten nested TOML to match pydantic field names
flat = {}
for section, values in toml_config.items():
if isinstance(values, dict):
for key, value in values.items():
flat[f"{section}_{key}"] = value
else:
flat[section] = values
return Settings(**flat)
Layered Configuration System
Multi-File Config with Overrides
import tomllib
from copy import deepcopy
from pathlib import Path
def deep_merge(base: dict, override: dict) -> dict:
"""Recursively merge override into base."""
result = deepcopy(base)
for key, value in override.items():
if (key in result
and isinstance(result[key], dict)
and isinstance(value, dict)):
result[key] = deep_merge(result[key], value)
else:
result[key] = deepcopy(value)
return result
class ConfigLoader:
"""Layered TOML configuration: defaults → environment → local → env vars."""
def __init__(self, base_dir: str = "config"):
self.base_dir = Path(base_dir)
def load(self, env: str = "development") -> dict:
config = {}
# Layer 1: defaults
config = self._load_file(self.base_dir / "defaults.toml", config)
# Layer 2: environment-specific
config = self._load_file(self.base_dir / f"{env}.toml", config)
# Layer 3: local overrides (gitignored)
config = self._load_file(self.base_dir / "local.toml", config)
return config
def _load_file(self, path: Path, base: dict) -> dict:
if path.exists():
with open(path, "rb") as f:
override = tomllib.load(f)
return deep_merge(base, override)
return base
# Usage
config = ConfigLoader().load(os.environ.get("APP_ENV", "development"))
Config File Structure
config/
├── defaults.toml # Shared defaults (committed)
├── development.toml # Dev overrides (committed)
├── staging.toml # Staging overrides (committed)
├── production.toml # Production overrides (committed)
└── local.toml # Personal overrides (gitignored)
pyproject.toml Mastery
Reading Your Own Package Config
import tomllib
from pathlib import Path
def get_project_version() -> str:
"""Read version from pyproject.toml."""
pyproject = Path(__file__).parent.parent / "pyproject.toml"
with open(pyproject, "rb") as f:
config = tomllib.load(f)
return config["project"]["version"]
Tool Configuration Patterns
[tool.myapp]
# Your application's tool-specific config
api_url = "https://api.example.com"
timeout = 30
retry_count = 3
[tool.myapp.features]
beta_dashboard = true
new_auth_flow = false
def load_tool_config() -> dict:
"""Load tool-specific config from pyproject.toml."""
with open("pyproject.toml", "rb") as f:
config = tomllib.load(f)
return config.get("tool", {}).get("myapp", {})
Programmatic pyproject.toml Editing
import tomlkit
def bump_version(new_version: str):
"""Update version in pyproject.toml preserving formatting."""
with open("pyproject.toml") as f:
doc = tomlkit.parse(f.read())
doc["project"]["version"] = new_version
with open("pyproject.toml", "w") as f:
f.write(tomlkit.dumps(doc))
def add_dependency(package: str):
"""Add a dependency to pyproject.toml."""
with open("pyproject.toml") as f:
doc = tomlkit.parse(f.read())
deps = doc["project"]["dependencies"]
if package not in deps:
deps.append(package)
with open("pyproject.toml", "w") as f:
f.write(tomlkit.dumps(doc))
tomlkit Internals
tomlkit preserves the full document structure including:
import tomlkit
doc = tomlkit.parse("""
# Application settings
[app]
name = "MyApp" # The app name
version = "1.0.0"
# Database configuration
[database]
host = "localhost"
port = 5432
""")
# Comments are preserved
doc["app"]["version"] = "2.0.0"
# Add new keys with comments
doc["app"].add(tomlkit.comment("Maximum concurrent users"))
doc["app"].add("max_users", 100)
print(tomlkit.dumps(doc))
# Comments, spacing, and ordering all preserved
tomlkit Types
tomlkit uses wrapper types that behave like Python types but carry formatting metadata:
import tomlkit
doc = tomlkit.parse('value = "hello"')
v = doc["value"]
type(v) # <class 'tomlkit.items.String'>
isinstance(v, str) # True — it behaves like str
v.trivia # Whitespace and comment metadata
Error Handling
Graceful Configuration Loading
import tomllib
import sys
from pathlib import Path
class ConfigError(Exception):
"""Configuration loading or validation error."""
pass
def load_config_safe(path: str) -> dict:
"""Load TOML config with clear error messages."""
config_path = Path(path)
if not config_path.exists():
raise ConfigError(
f"Configuration file not found: {path}\n"
f"Copy config.example.toml to {path} and edit it."
)
try:
with open(config_path, "rb") as f:
return tomllib.load(f)
except tomllib.TOMLDecodeError as e:
raise ConfigError(
f"Invalid TOML in {path}: {e}\n"
f"Check syntax at https://toml.io"
) from e
# Usage with graceful exit
try:
config = load_config_safe("config.toml")
except ConfigError as e:
print(f"ERROR: {e}", file=sys.stderr)
sys.exit(1)
Performance Notes
TOML parsing is fast because the format is simple:
| Library | Parse 100 KB file | Notes |
|---|---|---|
tomllib (3.11+) | ~2ms | C accelerated |
tomli | ~5ms | Pure Python |
tomlkit | ~15ms | Preserves formatting |
For configuration files (typically < 10 KB), parsing time is negligible. The choice between libraries should be driven by features, not speed.
One Thing to Remember
TOML + Pydantic is the modern Python configuration stack — TOML provides human-friendly, typed configuration files while Pydantic adds validation, defaults, and environment variable overrides, and tomlkit enables comment-preserving edits for tools that modify pyproject.toml.
See Also
- Python Csv Processing Learn how Python reads and writes spreadsheet-style CSV files — the universal language of data tables.
- Python Json Handling See how Python talks to the rest of the internet using JSON — the universal language apps use to share information.
- Python Template Strings See how Python's Template strings let you fill in blanks safely, like a Mad Libs game that can't go wrong.
- Ci Cd Why big apps can ship updates every day without turning your phone into a glitchy mess — CI/CD is the behind-the-scenes quality gate and delivery truck.
- Containerization Why does software that works on your computer break on everyone else's? Containers fix that — and they're why Netflix can deploy 100 updates a day without the site going down.