ledgerline
Plain-text double-entry accounting for people who want their money in version-controlled files. ledgerline reads a
strict, hledger-flavoured journal, refuses to accept anything that does not balance, points a caret at exactly what is
wrong when it fails, and turns the result into balance, register, income statement, balance sheet, cash flow and budget
reports with multi-commodity support, price-based valuation and Indian lakh digit grouping. Everything is
Decimal arithmetic; there is no database and no network access.
Features
- Strict journal format with precise
file:line:colerrors, the offending line, a caret, and hints (did-you-mean for undeclared accounts, difference for failed assertions). All syntax errors are reported in one pass. - Multi-commodity balancing with
@/@@costs, one auto-balanced elided posting (producing one posting per commodity when needed), virtual postings(A)and[A], tags, posting dates,includewith globs. - Balance assertions checked in date order, not file order.
- Price database from
Pdirectives plus implicit prices from costs; direct, inverse and one-hop lookup.--value INRat the report end date or historically (--value-at then),-Bfor cost basis. - Reports:
bal(tree/flat, depth, period columns),reg(running total),is,bs(with current earnings and unrealised gains),cf(operating/investing/financing/exchange),budget(periodic transactions, % used). - CSV importer with TOML rules (column mapping, date format, debit/credit or signed amounts, regex rules), duplicate detection, and canonical output.
- Canonical formatter
fmt(idempotent,--checkfor CI), query language (payee:,tag:,amt:>1000,date:2026-01,not:),stats,accounts,payees. - Output as rich tables,
csvorjsonfor every report. - Indian (
₹1,00,000.00) and western ($1,000,000.00) grouping, driven bycommoditydirectives.
Install
Requires Python 3.12+.
git clone <this repo> && cd ledgerline
uv venv .venv && uv pip install -p .venv/bin/python -e '.[dev]'
.venv/bin/ledgerline --help
(pip install -e . in any virtualenv works equally well.)
Quickstart
The repository ships a realistic year of finances in examples/ (410 transactions, 12 monthly files, prices, an
import CSV with rules).
export LEDGERLINE_FILE=examples/main.journal
ledgerline check # parse + balance + assertions + strict accounts
ledgerline bal --depth 2 # balances
ledgerline bal Expenses --quarterly # spending by quarter
ledgerline is --monthly # income statement
ledgerline bs --value INR # net worth, USD holdings and stocks valued in rupees
ledgerline budget --yearly # budget vs actual
ledgerline import examples/bank.csv --rules examples/rules.toml
Every command accepts -f/--file (or $LEDGERLINE_FILE), and -o table|csv|json.
Usage
All output below was produced by running the commands against examples/main.journal.
check
$ ledgerline check
OK: 410 transactions, 809 postings, 15 file(s)
Errors exit with status 1 and print one block per problem. Syntax errors (examples/broken-syntax.journal):
$ ledgerline check -f examples/broken-syntax.journal
error: invalid date '2026-02-30': 2026-02 has only 28 days
--> examples/broken-syntax.journal:8:1
|
8 | 2026-02-30 Impossible date
| ^^^^^^^^^^
error: misplaced thousands separator in '1,00.50'
--> examples/broken-syntax.journal:13:20
|
13 | Expenses:Food 1,00.50 INR
| ^^^^^^^
= hint: use 1,000 (western) or 1,00,000 (indian) grouping, or no commas
error: account name 'Expenses:Food 80 INR' contains an amount-like part
--> examples/broken-syntax.journal:17:5
|
17 | Expenses:Food 80 INR
| ^^^^^^^^^^^^^^^^^^^^
= hint: separate the account name from the amount with two or more spaces
3 errors found
Errors in the books themselves (examples/broken-books.journal, strict mode on):
$ ledgerline check -f examples/broken-books.journal
error: account 'Expenses:Fod' is not declared (strict mode)
--> examples/broken-books.journal:15:5
|
15 | Expenses:Fod 250.00 INR
| ^^^^^^^^^^^^
= hint: did you mean 'Expenses:Food'?
error: transaction does not balance: remaining 10.00 INR
--> examples/broken-books.journal:19:1
|
19 | 2026-01-10 Taxi
| ^^^^^^^^^^^^^^^
= hint: add the missing posting, or leave one amount out and it will be filled in automatically
2 errors found
A failed balance assertion (examples/broken-assertion.journal):
$ ledgerline check -f examples/broken-assertion.journal
error: balance assertion failed for Assets:Bank: expected 650.00 INR, actual 750.00 INR
--> examples/broken-assertion.journal:17:32
|
17 | Assets:Bank 0.00 INR = 650.00 INR
| ^^^^^^^^^^^^
= hint: difference is -100.00 INR (assertions are checked in date order, as of 2026-01-31)
1 error found
bal
Options: QUERY..., --depth N, --flat/--tree, --empty, -b/-e DATE, --monthly/--quarterly/--yearly,
-H/--historical (period columns show ending balances), --value COMMODITY, --value-at end|then, -B/--cost.
$ ledgerline bal --depth 2 --value INR
Balance
Account Balance
────────────────────────────────
Assets ₹29,32,033.71
Bank ₹16,61,575.68
Broker ₹7,85,994.81
Cash ₹47,212.10
Investments ₹4,37,251.12
Equity -₹11,58,030.00
Opening -₹11,58,030.00
Expenses ₹6,88,473.66
Entertainment ₹10,116.00
Food ₹1,59,551.17
Health ₹9,168.16
Insurance ₹21,500.00
Rent ₹3,00,000.00
Shopping ₹34,460.39
Transport ₹46,140.95
Travel ₹55,650.50
Utilities ₹51,886.49
Income -₹23,81,364.97
Bonus -₹60,000.00
Dividends -₹4,108.53
Interest -₹7,256.44
Salary -₹23,10,000.00
Total ₹81,112.40
The last line is the unrealised gain on the USD stock and mutual-fund holdings: assets are marked to market while equity stays at cost (see docs/design.md).
$ ledgerline bal Expenses --quarterly --depth 2
Balance
Account 2026Q1 2026Q2 2026Q3 2026Q4 Total
──────────────────────────────────────────────────────────────────────────────────────────
Expenses ₹2,06,050.91 ₹1,48,674.52 ₹1,74,430.97 ₹1,59,317.26 ₹6,88,473.66
Entertainment ₹2,529.00 ₹2,529.00 ₹2,529.00 ₹2,529.00 ₹10,116.00
Food ₹32,952.52 ₹41,218.30 ₹39,486.58 ₹45,893.77 ₹1,59,551.17
Health ₹2,612.50 ₹669.67 ₹1,115.53 ₹4,770.46 ₹9,168.16
Insurance 0 0 ₹21,500.00 0 ₹21,500.00
Rent ₹75,000.00 ₹75,000.00 ₹75,000.00 ₹75,000.00 ₹3,00,000.00
Shopping ₹10,163.49 ₹8,176.52 ₹9,462.31 ₹6,658.07 ₹34,460.39
Transport ₹13,868.57 ₹9,684.01 ₹11,257.98 ₹11,330.39 ₹46,140.95
Travel ₹55,650.50 0 0 0 ₹55,650.50
Utilities ₹13,274.33 ₹11,397.02 ₹14,079.57 ₹13,135.57 ₹51,886.49
Total ₹2,06,050.91 ₹1,48,674.52 ₹1,74,430.97 ₹1,59,317.26 ₹6,88,473.66
reg
$ ledgerline reg tag:trip=goa
Register
Date Description Account Amount Running total
─────────────────────────────────────────────────────────────────────────────────────────────────────────
2026-03-09 IndiGo | Flights BLR-GOI return Expenses:Travel:Flights ₹18,400.00 ₹18,400.00
Liabilities:CreditCard:ICICI -₹18,400.00 0
2026-03-09 Taj Holiday Village | 4 nights Expenses:Travel:Hotels ₹24,000.00 ₹24,000.00
Liabilities:CreditCard:ICICI -₹24,000.00 0
2026-03-14 Fisherman's Wharf | Dining out Expenses:Travel:Food ₹3,150.00 ₹3,150.00
Liabilities:CreditCard:ICICI -₹3,150.00 0
2026-03-14 Scooter rental Expenses:Travel:Transport ₹2,400.00 ₹2,400.00
Assets:Cash:Wallet -₹2,400.00 0
2026-03-15 Britto's | Dining out Expenses:Travel:Food ₹2,480.50 ₹2,480.50
Liabilities:CreditCard:ICICI -₹2,480.50 0
2026-03-16 Gunpowder | Dining out Expenses:Travel:Food ₹2,890.00 ₹2,890.00
Liabilities:CreditCard:ICICI -₹2,890.00 0
2026-03-16 Beach shack Expenses:Travel:Food ₹1,350.00 ₹1,350.00
Assets:Cash:Wallet -₹1,350.00 0
2026-03-17 Café Chocolatti | Dining out Expenses:Travel:Food ₹980.00 ₹980.00
Liabilities:CreditCard:ICICI -₹980.00 0
Queries combine: ledgerline reg Expenses:Rent date:2026-03 amt:">1000".
$ ledgerline reg Expenses:Rent date:2026-03 amt:">1000"
Register
Date Description Account Amount Running total
─────────────────────────────────────────────────────────────────────────
2026-03-02 Landlord | Rent Expenses:Rent ₹25,000.00 ₹25,000.00
is, bs, cf
$ ledgerline is --quarterly --depth 1
Income Statement
Account 2026Q1 2026Q2 2026Q3 2026Q4 Total
──────────────────────────────────────────────────────────────────────────────────────────
Revenues
Income ₹6,16,959.42 ₹5,86,786.25 ₹5,86,735.15 ₹5,86,775.62 ₹23,77,256.44
$13.52 $11.31 $11.47 $12.03 $48.33
Total revenues ₹6,16,959.42 ₹5,86,786.25 ₹5,86,735.15 ₹5,86,775.62 ₹23,77,256.44
$13.52 $11.31 $11.47 $12.03 $48.33
Expenses
Expenses ₹2,06,050.91 ₹1,48,674.52 ₹1,74,430.97 ₹1,59,317.26 ₹6,88,473.66
Total expenses ₹2,06,050.91 ₹1,48,674.52 ₹1,74,430.97 ₹1,59,317.26 ₹6,88,473.66
Net income ₹4,10,908.51 ₹4,38,111.73 ₹4,12,304.18 ₹4,27,458.36 ₹16,88,782.78
$13.52 $11.31 $11.47 $12.03 $48.33
$ ledgerline bs --value INR --depth 2
Balance Sheet
Account total
────────────────────────────────────────────────────────
Assets
Bank ₹16,61,575.68
Broker ₹7,85,994.81
Cash ₹47,212.10
Investments ₹4,37,251.12
Total assets ₹29,32,033.71
Liabilities
Total liabilities 0
Net worth ₹29,32,033.71
Equity
Opening ₹11,58,030.00
Current earnings ₹16,92,891.31
Unrealised gains / commodity differences ₹81,112.40
Total equity ₹29,32,033.71
Current earnings is the not-yet-closed profit; Unrealised gains closes the gap between market value and cost, so
Net worth always equals Total equity.
$ ledgerline cf --yearly --depth 1
Cash Flow
Account 2026
───────────────────────────────
Operating
Expenses -₹4,39,344.61
Income ₹23,77,256.44
$48.33
Total operating ₹19,37,911.83
$48.33
Investing
Assets -₹3,80,000.00
-$4,975.22
Total investing -₹3,80,000.00
-$4,975.22
Financing
Equity ₹9,03,000.00
$3,000.00
Liabilities -₹2,49,129.05
Total financing ₹6,53,870.95
$3,000.00
Exchange
Exchange -₹5,02,995.00
$6,000.00
Total exchange -₹5,02,995.00
$6,000.00
Net cash flow ₹17,08,787.78
$4,073.11
Cash at start 0
Cash at end ₹17,08,787.78
$4,073.11
budget
~ monthly transactions in main.journal define the budget; actuals roll up from sub-accounts.
$ ledgerline budget --yearly
Budget (actual / budget, % used)
Account 2026
────────────────────────────────────────────────────────────
Expenses:Entertainment ₹10,116.00 / ₹10,800.00 (94%)
Expenses:Food:Dining ₹65,304.75 / ₹72,000.00 (91%)
Expenses:Food:Groceries ₹94,246.42 / ₹1,20,000.00 (79%)
Expenses:Rent ₹3,00,000.00 / ₹3,00,000.00 (100%)
Expenses:Shopping ₹34,460.39 / ₹48,000.00 (72%)
Expenses:Transport ₹46,140.95 / ₹48,000.00 (96%)
Expenses:Utilities ₹51,886.49 / ₹66,000.00 (79%)
Total ₹6,02,155.00 / ₹6,64,800.00 (91%)
Cells over 100% are red, 80% and above yellow.
import
$ ledgerline import examples/bank.csv --rules examples/rules.toml
15 new, 3 duplicate(s) skipped, 4 uncategorized (marked !)
2027-01-01 * Acme Corp | NEFT CR-ACME CORP-SALARY JAN ; tag:salary, import:17f07fb9a69a
Assets:Bank:HDFC ₹1,95,000.00
Income:Salary
2027-01-02 * Landlord | UPI-LANDLORD-RENT JAN ; import:15ca623493ea
Assets:Bank:HDFC -₹25,000.00
Expenses:Rent
2027-01-03 * UPI-ZOMATO-ORDER-88123 ; tag:food, import:8b25f29826ca
Assets:Bank:HDFC -₹486.00
Expenses:Food:Dining
2027-01-04 * BESCOM | BESCOM ELECTRICITY BILL ; tag:utilities, import:c0c3cfb816f8
Assets:Bank:HDFC -₹2,284.50
...
15 new, 3 duplicate(s) skipped, 4 uncategorized (marked !)
The three December rows that already exist in the journal (typed by hand with the bank's narration) were skipped.
Add --append FILE to write into a journal; re-running the same import afterwards reports 0 new. Rows that no rule
matched go to Expenses:Uncategorized and are marked ! for review. Rules format:
[csv]
account = "Assets:Bank:HDFC"
currency = "INR"
date_column = "Date"
date_format = "%d/%m/%y"
description_column = "Narration"
debit_column = "Withdrawal Amt." # or: amount_column = "Amount" (+ invert = true)
credit_column = "Deposit Amt."
default_account = "Expenses:Uncategorized"
[[rule]]
match = "SWIGGY|ZOMATO" # regex, case-insensitive, first match wins
account = "Expenses:Food:Dining"
payee = "Swiggy" # optional
status = "*" # optional ("!" to flag for review)
tags = "tag:food" # optional
fmt, stats, accounts, payees
$ ledgerline fmt --check && echo canonical
canonical
ledgerline fmt prints the formatted root file; -w rewrites it and every included file; --check exits 1 if anything
would change.
$ ledgerline stats
Journal statistics
Metric Value
──────────────────────────────────────────────────────────────────────
Files 15
Transactions 410
Postings 809
First transaction 2026-01-01
Last transaction 2026-12-31
Span (days) 365
Accounts used 30
Accounts declared 31
Commodities AAPL, FLEXICAP, INR, MSFT, NIFTY50, USD
Payees 54
Price directives 84
Periodic transactions 1
Balance assertions 16
Pending (!) transactions 0
Transactions per month (avg) 34.2
Busiest month 2026-03 (43)
$ ledgerline accounts --tree --depth 2
Accounts
Account Postings Declared
─────────────────────────────────────
Assets 312 yes
Bank 183 yes
Broker 29 yes
Cash 75 yes
Investments 25 yes
Equity 2 yes
Opening 2 yes
Expenses 300 yes
Entertainment 36 yes
Food 103 yes
$ ledgerline payees
Payees
Payee Transactions First Last
──────────────────────────────────────────────────────────────
SIP 24 2026-01-05 2026-12-05
Uber 24 2026-01-15 2026-12-27
Chai Point 23 2026-01-10 2026-12-22
Local vegetables 19 2026-02-04 2026-12-09
Auto rickshaw 18 2026-01-09 2026-12-09
JSON and CSV output
Reports are emitted as tidy records (one row per account, period and commodity):
$ ledgerline bal Expenses --yearly --depth 2 -o csv
account,period,commodity,quantity
Expenses,2026,INR,688473.66
Expenses:Entertainment,2026,INR,10116.00
Expenses:Food,2026,INR,159551.17
Expenses:Health,2026,INR,9168.16
Expenses:Insurance,2026,INR,21500.00
Expenses:Rent,2026,INR,300000.00
Expenses:Shopping,2026,INR,34460.39
Expenses:Transport,2026,INR,46140.95
Expenses:Travel,2026,INR,55650.50
Expenses:Utilities,2026,INR,51886.49
Total,2026,INR,688473.66
Query language
Expenses:Food, payee:Uber, tag:trip=goa, amt:>1000, date:2026-03, status:!, not:Rent, all ANDed.
Full reference: docs/query-language.md.
Configuration
| Setting | Where |
|---|---|
| Journal file | -f/--file, or $LEDGERLINE_FILE |
| Strict accounts | option strict in the journal, or --strict |
| Commodity display | commodity directives (symbol, precision, grouping) |
| Import mapping and rules | rules.toml (see above) |
Cash accounts for cf |
--cash REGEX |
Dates on the command line are YYYY, YYYY-MM or YYYY-MM-DD; -b is inclusive and -e exclusive.
Exit codes: 0 ok, 1 journal/format errors (or fmt --check differences), 2 usage errors (bad query, missing -f).
The file syntax is documented in docs/journal-format.md, the algorithms (balancing, assertion ordering, price lookup, valuation modes, cash flow classification, import de-duplication) in docs/design.md.
Architecture
src/ledgerline/
cli.py click commands, option wiring, error -> exit code mapping
parser.py line-oriented parser -> Journal AST; collects errors with line/col
models.py Amount, Posting, Transaction, PeriodicTransaction, PriceDirective, Journal
numfmt.py commodity formats, lakh/western grouping, Decimal rendering
booking.py balancing, elision, strict accounts, date-ordered assertions
prices.py PriceDB: P directives + implicit prices, direct/inverse/one-hop lookup
query.py query language -> predicates over (transaction, posting)
periods.py partial dates, month/quarter/year columns, recurrence expansion
reports.py entries, valuation, aggregation, bal/reg/is/bs/cf/budget
output.py Report container; rich table, csv and json emitters
importer.py rules.toml, CSV reading, de-duplication, transaction building
formatter.py canonical transaction rendering and file formatting
info.py stats, accounts, payees
errors.py LedgerError (caret rendering), JournalError, UsageError
scripts/gen_examples.py deterministic generator for examples/
Tests
uv pip install -p .venv/bin/python -e '.[dev]'
.venv/bin/pytest
130 tests cover every directive and parser error with line/column, balancing (multi-commodity, costs, virtual
postings), assertion ordering, price chains, the query language, each report's numbers on a small fixture, import
de-duplication, formatter idempotency, and end-to-end CLI runs against examples/.
Limitations
- Balancing is exact (no display-precision tolerance); use
@@when a unit price does not multiply out. - Price chains are limited to one intermediate commodity; amounts without a path stay in their own commodity.
bsignores-b; balances always accumulate from the start of the journal. There are no closing entries or period-end retained-earnings transfers; "current earnings" is computed.- Cash-flow categories are derived from account types (credit-card payments appear as financing).
- Secondary dates (
DATE=DATE2), lot tracking / capital-gains realisation, automated transactions and a--forecastview of periodic transactions are not implemented. - The CSV importer handles one statement account per rules file and decimal-point numbers.
- Regexes in queries use Python syntax, matched case-insensitively.
License
MIT, see LICENSE.