finance_md
SQLite-backed finance management for multiple accounts.
The database is the source of truth — every workspace stores its data in
a robust SQLite file (finance.db), and plain, readable, renderable
Markdown files (index.md plus one file per account) are generated from it
after every change as a visualization you can read, diff and render.
finances/
finance.db <- the real database (SQLite)
index.md <- generated overview (refreshed after every change)
accounts/
checking.md <- generated ledger view per account
savings.md
Why
- Dumbest-user-proof storage: ledger data lives in SQLite, not in files
people like to edit. A mangled, half-deleted or typo-ridden
.mdfile can never corrupt your books — the next command regenerates it from the database. - The
.mdviews stay ordinary Markdown: readable, renderable in any preview, and git-diffable after every change (deterministic writes, rows sorted by date, then Ref). - Real transactional safety: transfers write both accounts in one atomic SQLite transaction; balances are computed on read, never stored.
- Hand-editing is no longer a supported workflow. To change data, use the
CLI (
add,edit,delete,transfer) — it validates every field.
Install
pip install finance_md
Requires Python 3.9+. The only runtime dependency is PyYAML (used to render and to import v0.1 workspaces); SQLite is part of the Python standard library.
Quickstart
finance-md init # creates ./finances
cd finances
finance-md account add Checking --type bank --currency EUR
finance-md account add Savings --type savings --currency EUR
finance-md add Checking 2026-09-01 "Salary" 2500.00 --category income
finance-md add Checking 2026-09-03 "Grocery" -23.10 --category food
finance-md transfer Checking Savings 100 --date 2026-09-05
finance-md summary
finance-md validate
Amounts use the sign convention income + / expense -, two decimal
places, no thousands separators.
Architecture
finance.dbis a SQLite database with anaccountstable (id, name, type, currency, created, archived) and atransactionstable (account, ref, date, description, category, amount), plus a schema version marker. Delete it and you have lost your data — back it up like any database.- Every mutating command writes the database and then regenerates the
.mdviews. Views are treated as caches: hand edits are never read back and are silently overwritten. finance-md validatechecks that the views match the database and reports missing, out-of-sync or orphaned view files.finance-md renderregenerates the views without changing any data — useful after you (or an editor plugin) mangled them.
View format
The generated files are plain Markdown. A generated banner marks them as machine-owned:
<!-- GENERATED FILE - do not edit. The database (finance.db) is the source of truth; any change here is overwritten by the next finance-md command. -->
---
id: 3f9c2a1b
name: Checking
type: bank # bank | cash | credit | savings | other
currency: EUR
created: 2026-09-19
archived: false
---
# Checking
## Transactions
| Ref | Date | Description | Category | Amount |
|---|---|---|---|---:|
| 3f9c2a1b | 2026-09-01 | Salary | income | 2500.00 |
| a1b2c3d4 | 2026-09-03 | Grocery | food | -23.10 |
Format rules (as produced by the tool):
- Ref: 8-hex-character stable id; transfers share the same Ref across the two account views, which is the transfer linkage.
- Amount: decimal with exactly 2 places; income
+, expense-. - Balance: computed on read (sum of Amount), never stored.
- Files are written with
\nand rows sorted by date, then Ref, so git diffs stay minimal.
Migrating from v0.1 (markdown-only workspaces)
v0.1 stored the data directly in the .md files. Convert an old workspace:
finance-md import-md OLD_DIR [DEST] # DEST defaults to ./finances
The old workspace is parsed with the strict v0.1 parser; any problem (bad amount, bad date, duplicate account id, name or filename slug) aborts the import before anything is created, and a failed import rolls the destination back. The source directory is never written to, and importing a directory into itself is refused — delete the old directory after you are satisfied with the converted workspace.
CLI reference
| Command | Description |
|---|---|
finance-md init [DIR] |
create a workspace (default ./finances) |
finance-md account add NAME --type TYPE --currency CODE |
create an account |
finance-md account list |
list accounts with balances |
finance-md account archive NAME |
archive an account |
finance-md add ACCOUNT DATE DESCRIPTION AMOUNT [--category C] |
add a transaction |
finance-md list ACCOUNT [--month YYYY-MM] [--category C] |
list transactions |
finance-md edit ACCOUNT REF [--date --description --category --amount] |
edit by ref |
finance-md delete ACCOUNT REF |
delete by ref |
finance-md transfer FROM TO AMOUNT [--date --description] |
move money (same currency) |
finance-md summary [--month YYYY-MM] |
balances, archived accounts, category totals |
finance-md validate |
check views against the database (non-zero exit on issues) |
finance-md render |
regenerate the .md views from the database |
finance-md import-md SRC [DEST] |
import a v0.1 markdown-only workspace |
Every command accepts --workspace PATH (before or after the command).
Exit codes: 0 ok, 1 error, 2 usage.
Workspace resolution order: --workspace flag, then FINANCE_MD_WORKSPACE
environment variable, then the current directory if it contains
finance.db; otherwise an error tells you to run finance-md init.
Archived accounts reject new transactions and transfers; edit/delete
still work for corrections, and summary lists archived accounts separately.
Library usage
from decimal import Decimal
from finance_md import Workspace, service
ws = Workspace.open("~/finances") # or Workspace.init(path)
ws.create_account("Checking", "bank", "EUR")
service.add_tx(ws, "Checking", date(2026, 9, 1), "Salary", Decimal("2500.00"), category="income")
out_tx, in_tx = service.transfer(ws, "Checking", "Savings", Decimal("100.00"))
for meta, txs in ws.load_all():
print(meta.name, meta.currency, sum(t.amount for t in txs))
issues = service.validate(ws) # [] means the views match the database
Lower layers: finance_md.db.Database is the SQLite repository, views
renders the .md files, and finance_md.store keeps the v0.1 strict
parser/serializer (used by import-md). finance_md.errors defines
FinanceMDError, WorkspaceError, ParseError(file, line) and
NotFoundError.
Concurrency
SQLite gives you real multi-process safety on one machine: reads are
concurrent, writes take a short exclusive lock, and each multi-account
operation is atomic. Working from several machines at once (e.g. a synced
folder) is still out of scope — pick one writer at a time. Keep the
workspace in git or a backup routine: finance.db holds the data, and the
generated views diff nicely.
Development
pip install -e ".[dev]"
pytest
ruff check .
mypy src
python -m build && twine check dist/*
Roadmap (out of scope in v0.2)
CSV import/export, budgets, multi-currency/FX, recurring transactions, custom table columns, interest calculations, workspace-level encryption.
License
MIT — see LICENSE.
Release files for finance-md 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| finance_md-0.2.0.tar.gz | 36.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| finance_md-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 66.6 kB
Release files / finance_md-0.2.0.tar.gz
| Download URL | finance_md-0.2.0.tar.gz |
|---|---|
| Size | 36.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
82c21bb4c685f696a779cf3ed97e13585fe64790def584acf8ff1a34261cbdb1
|
|
BLAKE2b-256 checksum How to use checksums |
9bbc18b4eaf86ee4d6a332c0a268b91adfc1dec30295ec602aaea35f6770a0c5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 20, 2026.
Transparency logRelease files / finance_md-0.2.0-py3-none-any.whl
| Download URL | finance_md-0.2.0-py3-none-any.whl |
|---|---|
| Size | 30.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
1cb3e0cc2a7242c098b73cd4387dd1fc737bb2b43c5070570f888e5f75e2bb7a
|
|
BLAKE2b-256 checksum How to use checksums |
1618fa6a166d7aa23709d4b9cd7c7579863877ce6fa6da5036dbbdf2e2e1d10a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 20, 2026.
Transparency log