← Home

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 cfg

config.toml format

# RAM Manager config
threshold     = 80
top_n         = 5
model         = "claude-3-haiku-20240307"
poll_interval = 5

[llm]
timeout = 30
max_retries = 3

Config 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.'
    )

Resources