I love learning new things. Rabbit holes can be fun. This year I dug into double-entry accounting. And I didn’t just learn the theory; I decided to use double-entry accounting, with some open source plain text accounting software, for personal finances.
I’ll explain what I mean by these terms and what I have been up to in this post.
- Double-entry accounting: An accounting method where every transaction is recorded against at least two accounts.
- Plain text accounting: An accounting method where all your data is recorded in plain text, making the data portable and scriptable.
- Beancount: An open source accounting package, written in Python, which implements double-entry accounting in plain text.
First, I got curious about double-entry accounting. My wife is an accountant and I asked her questions, and I also read a few articles online. The most confusing part to me was the language of “debits” and “credits,” although I found that to be unimportant to understanding the essence of double-entry accounting. To remember what those terms mean, the best tip I got was from my wife; she said to focus on what is happening with cash: increasing cash is a debit, and decreasing it is a credit. From there, you can figure out what term to use (the opposite) for the other part of a transaction.
Second, I got curious about plain text accounting. I wanted to find a way for my wife and I to track our personal finances, and I was leary of another SaaS app; I really wanted to own this data and easily analyze it, and I didn’t want to be locked into a subscription or a vendor who might be here today and gone tomorrow. (As I publish this, Mint is going away.) The more I read about plain text accounting, the more I was interested to give it a shot, although I was unsure where to begin.
Third, after doing research on different plain text accounting systems, I decided to try Beancount. I considered hledger and ledger too, but beancount has a really slick web interface called Fava, which was my main reason for trying it first. It also has amazing documentation and is easy to extend in Python, which is a language I like, so that was more reason to try it.
So what?
At the end of all my research, and after implementing this system, what is the impact?
Here are some outcomes:
- All our transactions since 2022 are now in a single text file.
- Analysis of financial data (spending, giving, taxes, etc.) is trivial. It’s easy to do with Fava, but it’s also easy to do with scripts. For instance, I have one Python script that pulls data out of Beancount using the Beancount Query Language, and then I use a pandas dataframe to prepare the data into a report.
- Every month I do a reconciliation process on all the accounts which would help catch any unusual activity.
- I have a growing collection of code, written in Python, to simplify the monthly reconciliation process, and eliminate much of the manual work for importing data.
Beancount ledger example snippet
Perhaps the easiest way to grok this is to see some example syntax. Here is a short chunk of lines from my ledger, altered a bit, from a work trip to Toronto:
pushtag #toronto-trip-2023
2023-08-11 * "Hotel Name Here" "DESCRIPTION FROM STATEMENT"
Liabilities:US:CreditCardName -500.49 USD
Expenses:Travel:Hotel
2023-08-11 * "Pittsburgh Airport" "PIT PARKING/LAZ-2"
Liabilities:US:CreditCardName -50.00 USD
Expenses:Auto:Parking
poptag #toronto-trip-2023
Some things to note:
- The
pushtag
andpoptag
syntax can be used to apply tags to all the transactions that appear in between. I mainly use tags for trips. This makes it easy to find all the transactions. For a work trip, it can help catch expenses that weren’t reimbursed; you would expect to see net $0 for a work trip, if everything was expensable. - Each transaction has a date, a payee, and a description. Note that the payee is optional and I don’t always add one.
- All of the accounts referenced (
Liabilities:US:CreditCardName
,Expenses:Travel:Hotel
, andExpenses:Auto:Parking
) were defined earlier in the file with anopen
directive, to indicate that the account was opened. You have complete flexibility in designing your accounts; some conventions are suggested here. I have one account for rent,Expenses:Home:Rent
, that I marked as closed with theclose
directive; since we bought a house and are no longer paying rent, it didn’t make sense to leave that account open. Similarly, I close annual tax accounts after all transactions for that tax year are complete. - In this case, each of the transactions was on a single credit card account, and applied to a single expense account, but it would have been possible to apply several expense accounts if we had wanted.
Let’s say that at the hotel, I had ordered $10 worth of food; I could change the above transaction as follows:
2023-08-11 * "Hotel Name Here" "DESCRIPTION FROM CREDIT CARD COMPANY"
Liabilities:US:CreditCardName -500.49 USD
Expenses:Food:Restaurants 10.00 USD
Expenses:Travel:Hotel
Now, instead of booking the entire transaction against the Hotel expense category, we are representing that some of the transaction was for food. With Beancount, you have the option of specifying the amount of each item; for many transactions, I only record one of the amounts, as the other is just the inverse.
My beancount repository
I’m using a git repository for this data, hosted on a homelab server. It has the following files:
- README.md: Documentation of the project.
- justfile: Some commands that I like to use for my bookkeeping. (If you’re unfamiliar with just, it’s worth a look.)
- watt.beancount: The ledger itself.
- tmp.beancount: This file is gitignored, but is where I dump all the automatically prepared text entries for transactions extracted from my data sources (banks and paystubs, mainly).
- watt.import: This file sets up the custom importers.
And the following directory structure:
- documents/: This is where source documents (e.g., CSV files from banks or mortgage company) are archived, under subfolders corresponding to the account name.
- importers/: This is where the Python code for custom importers lives, along with tests.
- queries/: This is where I put a script to generate a report that I wanted.
And here’s the justfile, just in case it’s useful:
check:
bean-check watt.beancount
extract:
bean-extract -f watt.beancount -e watt.beancount watt.import ~/Downloads > tmp.beancount
fava:
fava watt.beancount
file:
bean-file watt.import ~/Downloads -o documents
alias fmt := format
format:
bean-format -o watt.beancount watt.beancount
black .
identify:
bean-identify watt.import ~/Downloads
my-custom-report-name:
python3 queries/some_filename_here.py
test:
pytest
What the monthly process looks like
It’s not hard to update the ledger each month. Here’s what I do:
- Download CSV files from relevant parties. There’s one for every credit card, one for bank accounts, one for our mortgage, and I even found that my employer supplies CSV files with paystub information.
- Run
just extract
. This command looks through the files in my Downloads folder, uses the code in the importers to identify which file maps to which custom importer (most of mine just use filename to map this, but you could also open the file and apply some test), and then uses the importer code to construct the transactions and dumps them intotmp.beancount
. - Review entries from
tmp.beancount
and copy/paste them into thewatt.beancount
file. Technically, you can put entries anywhere in the file, but I like to organize them; I have a section in the file for each account. - Run
just check
to make sure everything still balances. - Run
just file
to automatically file the CSV source files into the appropriate subfolder in thedocuments/
directory. - Run
just fmt
to format my ledger.
How I write importers
Beancount has a nice guide so I won’t explain this too much, but what I did to develop my importers is I downloaded a CSV file from my bank, removed a lot of the transactions so it was a smaller file to work with, and wrote failing tests using Beancount’s test helpers. My wrapper just test
calls pytest
and will let me see the failures. Then, I write the code that is missing to get everything to pass.
I think one of the best things to do when writing an importer is to generate an assertion about the account balance automatically. An assertion is just another line in your file, which looks something like this:
2023-08-01 balance AccountName 100.00 USD
That assertion is saying, on this date, the opening balance in the account was $100.00. This will save you a ton of time if you automate these entries, because one of the things bean-check
does is verify that all of these balance assertions are correct. Then if you miss any transactions, or double-count something, you will have an error that a specific balance assertion failed.
Automating transaction categorization
I’ve been using smart_importer to automatically fill out some transaction information; sometimes it can guess the payee or the expense category correctly, for instance. Sometime I’d like to play with it more, to see if I can improve the automated detection. I might also write more rules in Python so that I can hardcode specific things, instead of relying on ML to get it right some fraction of the time.
Other nifty things
I work at a publicly traded company and get stock grants. It’s nice that Beancount can track quantities in any currency, whether USD or a ticker symbol. This was something that would have been annoying to do with Mint; the equity platform used at work didn’t integrate with Mint. I had been using a spreadsheet to understand my equity position, but it’s really nice to just have this in the same system used for all other transactions.
Why I dig this
I really like having my data in such a convenient format, and I like that I can automate things to my heart’s content. There is no API like a file on your disk; it’s easy to integrate with.
If you liked this
If you liked this, here are some other resources you might like:
- Beancount documentation: It’s really comprehensive. I hope that my blog post adds something, though; I found it a little hard to get a TLDR on the whole thing and perhaps this post can help with that a bit.
- Fava: A beautiful Web interface for Beancount.
- The Five-Minute Ledger Update: I found this site really helpful when I was getting set up.
- Awesome Beancount: GitHub repo with a bunch of useful links.
Also if this is useful to you, or you have questions about my setup, please get in touch! The easiest way is probably Twitter.