134. Tomllib And Config Files
User configuration with TOML files
134. Tomllib And Config Files
⚙️ Aryan wants users to configure the RAM manager via ~/.ram_manager/config.toml instead of always passing CLI flags. His config loader ignores missing files, uses the wrong tomllib function, and doesn’t merge with defaults properly.
💡 Fun fact: TOML was created by Tom Preston-Werner, co-founder of GitHub and creator of the Semantic Versioning spec. He named it after himself: Tom’s Obvious, Minimal Language. Python’s own packaging standard (pyproject.toml) switched from setup.cfg to TOML in PEP 517/518. Since Python 3.11, tomllib is built into the standard library — no pip install needed.
⚠️ Watch out: tomllib is read-only — it has no dump() function. This surprises almost everyone. The Python core devs made a deliberate decision to only include the parser in stdlib. To write TOML, use tomli_w (pip install) or just write the config string manually. Calling tomllib.dump() raises AttributeError at runtime, not a nice import error.
🤔 Think about it: The dict.update() merge approach overwrites every key, including ones that weren’t in the user’s config file. Why is that a problem? Imagine a user’s config only sets threshold = 70 — what happens to the default value of top_n if you use config.update(user_config) and user_config doesn’t contain top_n?
Learning objectives
- Read TOML files with tomllib.load() in binary mode
- Return defaults gracefully when config file is missing
- Merge user config with defaults without overwriting non-set keys
- Write TOML config files using template strings (tomllib is read-only)
- Use ~/.app_name/ as the standard config directory
Key concepts
- tomllib.load(f) — read TOML (binary mode required)
- tomllib is read-only — no tomllib.dump()
- dict.copy() + selective update — safe config merging
- Path.home() / ‘.app’ — XDG-style config directory
- TOML format — human-readable config syntax
Try it
Concept detail
Configuration Files with TOML
TOML (Tom’s Obvious, Minimal Language) is the standard for Python config files (it’s what pyproject.toml uses).
Reading TOML (Python 3.11+ built-in)
import tomllib
with open('config.toml', 'rb') as f: # MUST be binary mode
config = tomllib.load(f)
# Or from a string
config = tomllib.loads('[tool]\nname = "ram_manager"')Writing TOML (manual or tomli_w)
# Option 1: Manual template string
TEMPLATE = '''
threshold = 80
model = "claude-3-haiku-20240307"
'''
Path('config.toml').write_text(TEMPLATE)
# Option 2: pip install tomli_w
import tomli_w
with open('config.toml', 'wb') as f:
tomli_w.dump({'threshold': 80}, f)Merging user config with defaults
DEFAULTS = {'threshold': 80, 'model': 'haiku', 'top_n': 5}
def load(path):
cfg = DEFAULTS.copy()
if path.exists():
with open(path, 'rb') as f:
user = tomllib.load(f)
# Only override keys that are set and valid
cfg.update({k: v for k, v in user.items() if k in DEFAULTS and v is not None})
return cfgconfig.toml format
# RAM Manager config
threshold = 80
top_n = 5
model = "claude-3-haiku-20240307"
poll_interval = 5
[llm]
timeout = 30
max_retries = 3Config file location convention
from pathlib import Path
CONFIG_DIR = Path.home() / '.ram_manager'
CONFIG_FILE = CONFIG_DIR / 'config.toml'Solution
import tomllib
from pathlib import Path
DEFAULT_CONFIG = {
'threshold': 80,
'top_n': 5,
'model': 'claude-3-haiku-20240307',
'poll_interval': 5,
'report_dir': str(Path.home() / '.ram_manager' / 'reports'),
}
CONFIG_PATH = Path.home() / '.ram_manager' / 'config.toml'
TOML_TEMPLATE = '''\
# RAM Manager Configuration
# Edit these values to customize the tool.
threshold = 80 # Alert when RAM exceeds this % (1-100)
top_n = 5 # Number of top processes to show
model = "claude-3-haiku-20240307"
poll_interval = 5 # Seconds between RAM checks
'''
def load_config(path: Path = CONFIG_PATH) -> dict:
"""Load config from TOML file, falling back to defaults."""
config = DEFAULT_CONFIG.copy()
if not path.exists():
return config # first run — use defaults, no crash
with open(path, 'rb') as f: # tomllib requires binary mode
user_config = tomllib.load(f)
# Only override keys that are actually present in user_config
# (don't let None values from partial configs overwrite defaults)
for key, value in user_config.items():
if value is not None and key in DEFAULT_CONFIG:
config[key] = value
return config
def save_default_config(path: Path = CONFIG_PATH):
"""Write a default config.toml for first-time users."""
path.parent.mkdir(parents=True, exist_ok=True)
# tomllib is read-only — write TOML manually or use tomli_w
path.write_text(TOML_TEMPLATE)Tests
import tempfile
import tomllib
from pathlib import Path
def test_load_config_returns_defaults_when_missing():
with tempfile.TemporaryDirectory() as d:
missing = Path(d) / 'nonexistent.toml'
config = load_config(missing)
assert config['threshold'] == 80
assert config['top_n'] == 5
def test_load_config_overrides_with_user_values():
with tempfile.TemporaryDirectory() as d:
cfg_file = Path(d) / 'config.toml'
cfg_file.write_bytes(b'threshold = 70\ntop_n = 10\n')
config = load_config(cfg_file)
assert config['threshold'] == 70
assert config['top_n'] == 10
# Defaults preserved for unset keys
assert config['model'] == 'claude-3-haiku-20240307'
def test_load_config_preserves_defaults_for_missing_keys():
with tempfile.TemporaryDirectory() as d:
cfg_file = Path(d) / 'config.toml'
cfg_file.write_bytes(b'threshold = 90\n') # only threshold set
config = load_config(cfg_file)
assert config['poll_interval'] == 5 # default preserved
def test_save_default_config_creates_file():
with tempfile.TemporaryDirectory() as d:
path = Path(d) / 'subdir' / 'config.toml'
save_default_config(path)
assert path.exists(), 'save_default_config must create the file'
def test_save_default_config_creates_valid_toml():
with tempfile.TemporaryDirectory() as d:
path = Path(d) / 'config.toml'
save_default_config(path)
content = path.read_bytes()
parsed = tomllib.loads(content.decode())
assert isinstance(parsed, dict)
def test_load_config_does_not_use_tomllib_dump():
import inspect
src = inspect.getsource(save_default_config)
assert 'tomllib.dump' not in src, (
'tomllib is read-only — it has no .dump(). '
'Use path.write_text(toml_string) or pip install tomli_w.'
)