← Home

148. Tomllib (App V0.9)

RAM Manager v0.9 — user config with TOML

148. Tomllib (App V0.9)

Aryan is tired of typing --threshold 85 every time. He adds a config file.

🤔 Socratic question: How does VS Code remember your settings? How does git know your email? How does npm know your project name? Config files. Every serious tool has one. Time to add yours.

🏛️ History: TOML was created in 2013 by Tom Preston-Werner (co-founder of GitHub) specifically for configuration files. It’s now the standard config format for Rust packages (Cargo.toml), Python packages (pyproject.toml), and more. GitHub itself uses TOML for internal configs. You’re using something co-created by the guy who built GitHub.

# ~/.ram_manager/config.toml
threshold = 85
top_n     = 15
model     = "claude-haiku-4-5-20251001"

Python 3.11+ has tomllib built-in. One catch: it requires binary mode.

import tomllib

with open(path, 'rb') as f:   # 'rb' required — tomllib needs bytes
    user_config = tomllib.load(f)

Opening without 'rb' raises TypeError. This catches everyone the first time.

The merge pattern (config file can be partial — fill in defaults for missing keys):

config = DEFAULT_CONFIG.copy()
for k, v in user_config.items():
    if k in config and v is not None:
        config[k] = v

Writing TOML: tomllib is read-only. There’s no tomllib.dump(). Write a template string:

path.write_text('''
threshold = 80
top_n = 10
model = "claude-haiku-4-5-20251001"
''')

CLI flag priority: CLI > config file > defaults

t = threshold if threshold is not None else config['threshold']

💡 Real-world: This exact priority pattern — CLI flags override config file, config file overrides defaults — is used by git, npm, docker, kubectl, and virtually every serious command-line tool ever built. You just implemented it yourself.

💡 Fun fact: tomllib was added to the Python standard library in Python 3.11 (2022) — but only the reader, not the writer. The decision to make it read-only was intentional: TOML files are for humans to edit, not for programs to generate. The format name stands for “Tom’s Obvious Minimal Language” — Tom being Tom Preston-Werner.

⚠️ Watch out: tomllib is read-only — there is no tomllib.dump(). Calling it raises AttributeError. To write a config file, use path.write_text(template_string) with a manually written TOML template. If you need programmatic TOML writing, install tomli_w separately.

🤔 Think about it: The load_config() solution uses DEFAULT_CONFIG.copy() first, then overlays user values. Why .copy() and not just DEFAULT_CONFIG? What would happen across multiple calls if you mutated DEFAULT_CONFIG directly? This is the same reason you never use a mutable default argument in Python functions.


⚙️ Aryan adds a config file so users can set their defaults once instead of passing flags every time. He opens the TOML file in text mode (tomllib needs binary), doesn’t merge with defaults, and tries to use tomllib.dump() which doesn’t exist.

Learning objectives

  • Open TOML files in binary mode (‘rb’) for tomllib
  • Merge user config with defaults (don’t discard unknown-key defaults)
  • Write TOML as a template string (tomllib is read-only)
  • CLI args override config only when explicitly provided (not None)

Key concepts

  • open(path, ‘rb’) — binary mode required by tomllib
  • tomllib.load(f) — parse TOML file
  • DEFAULT_CONFIG.copy() — start from defaults
  • Path.write_text(template) — write TOML (no tomllib.dump)
  • CLI > config > defaults — priority order

Try it

Concept detail

App v0.9 — TOML config file

Users can now configure RAM Manager once instead of typing flags each time:

# ~/.ram_manager/config.toml
threshold = 70
top_n     = 5
model     = "claude-haiku-4-5-20251001"
# First time setup
$ python ram_manager.py init-config
Config written to /home/aryan/.ram_manager/config.toml

# Now runs with threshold=70 automatically
$ python ram_manager.py monitor

# CLI still overrides config
$ python ram_manager.py monitor --threshold 90

Config merge pattern

# 1. Start with defaults
config = DEFAULT_CONFIG.copy()

# 2. Overlay user config (only for known keys)
with open(path, 'rb') as f:
    user = tomllib.load(f)
for k, v in user.items():
    if k in config and v is not None:
        config[k] = v

# 3. CLI flags override everything
threshold = cli_threshold if cli_threshold is not None else config['threshold']

Solution

import tomllib
import click
import psutil
from pathlib import Path
from datetime import datetime

CONFIG_PATH = Path.home() / '.ram_manager' / 'config.toml'

DEFAULT_CONFIG = {
    'threshold': 80,
    'top_n': 10,
    'model': 'claude-haiku-4-5-20251001',
}

CONFIG_TEMPLATE = '''\
# RAM Manager Configuration
# Edit these values to customize behaviour.

threshold = 80         # Alert when RAM exceeds this % (1-100)
top_n     = 10         # Number of top processes to show
model     = "claude-haiku-4-5-20251001"
'''

def load_config(path: Path = CONFIG_PATH) -> dict:
    config = DEFAULT_CONFIG.copy()
    if not path.exists():
        return config

    with open(path, 'rb') as f:   # tomllib requires binary mode
        user_config = tomllib.load(f)

    # Only override keys that exist in defaults and are not None
    for key, value in user_config.items():
        if key in DEFAULT_CONFIG and value is not None:
            config[key] = value
    return config

def save_default_config(path: Path = CONFIG_PATH) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    # tomllib is read-only — write the template string manually
    path.write_text(CONFIG_TEMPLATE)

@click.group()
@click.pass_context
def cli(ctx):
    '''RAM Manager — monitor memory and get AI advice.'''
    ctx.ensure_object(dict)
    ctx.obj['config'] = load_config()

@cli.command()
@click.option('--threshold', type=int, default=None, help='Override config threshold')
@click.option('--top-n', type=int, default=None, help='Override config top_n')
@click.pass_context
def monitor(ctx, threshold, top_n):
    '''Monitor RAM usage.'''
    config = ctx.obj['config']
    # CLI args override config only when explicitly provided (not None)
    final_threshold = threshold if threshold is not None else config['threshold']
    final_top_n = top_n if top_n is not None else config['top_n']
    click.echo(f'threshold={final_threshold} top_n={final_top_n} model={config["model"]}')

@cli.command('init-config')
def init_config():
    '''Write default config to ~/.ram_manager/config.toml'''
    save_default_config()
    click.echo(f'Config written to {CONFIG_PATH}')

if __name__ == '__main__':
    cli()

Tests

import tomllib
import inspect
import tempfile
from pathlib import Path
from click.testing import CliRunner

def test_load_config_returns_defaults_when_missing():
    with tempfile.TemporaryDirectory() as d:
        config = load_config(Path(d) / 'nonexistent.toml')
    assert config['threshold'] == 80
    assert config['top_n'] == 10

def test_load_config_overrides_with_user_values():
    with tempfile.TemporaryDirectory() as d:
        cfg = Path(d) / 'config.toml'
        cfg.write_bytes(b'threshold = 70\n')
        config = load_config(cfg)
    assert config['threshold'] == 70
    assert config['top_n'] == 10   # default preserved

def test_load_config_requires_binary_mode():
    src = inspect.getsource(load_config)
    assert "'rb'" in src or '"rb"' in src, 'tomllib requires binary mode: open(path, "rb")'

def test_save_default_config_creates_file():
    with tempfile.TemporaryDirectory() as d:
        path = Path(d) / 'config.toml'
        save_default_config(path)
        assert path.exists()

def test_save_default_config_creates_valid_toml():
    with tempfile.TemporaryDirectory() as d:
        path = Path(d) / 'config.toml'
        save_default_config(path)
        with open(path, 'rb') as f:
            parsed = tomllib.load(f)
        assert 'threshold' in parsed

def test_save_default_config_no_tomllib_dump():
    src = inspect.getsource(save_default_config)
    assert 'tomllib.dump' not in src, 'tomllib.dump() does not exist — use write_text()'

def test_monitor_uses_config_defaults():
    runner = CliRunner()
    result = runner.invoke(cli, ['monitor'])
    assert 'threshold=80' in result.output

def test_monitor_cli_overrides_config():
    runner = CliRunner()
    result = runner.invoke(cli, ['monitor', '--threshold', '60'])
    assert 'threshold=60' in result.output

Resources