The default answer to "where do I store state" has become a managed database with a monthly invoice. For self-hosted developer tools that is overkill: SQLite is transactional, battle-tested and already installed everywhere Python runs.
One file is a feature
A database you can copy with cp, back up with a cron job and inspect with a single binary changes how you operate. Enable WAL mode and you get concurrent readers with a writer — more than enough for a single-node tool.
import sqlite3
conn = sqlite3.connect("tool.db")
conn.execute("PRAGMA journal_mode=WAL")
conn.execute("PRAGMA foreign_keys=ON")Migrations without a framework
You do not need Alembic for a single-file database. A user_version pragma and an ordered list of migration functions gives you safe, forward-only schema upgrades in thirty lines.
MIGRATIONS = [
"CREATE TABLE events (id TEXT PRIMARY KEY, ts INTEGER)",
"ALTER TABLE events ADD COLUMN payload TEXT",
]
def migrate(conn):
v = conn.execute("PRAGMA user_version").fetchone()[0]
for i, sql in enumerate(MIGRATIONS[v:], start=v):
conn.execute(sql)
conn.execute(f"PRAGMA user_version={i + 1}")When not to use SQLite
Be honest about the ceiling: many writers across many machines is Postgres territory, not SQLite. But for CLIs, agents and single-node services — the vast majority of developer tools — the embedded model removes an entire category of operational failure.
That is why the whole PyForge catalog is built this way: your tool should run on a five-dollar VPS or a laptop with equal confidence.