Alex Watt

About Blog Hacks Recommendations

Beancount for Personal Finance

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.

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:

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

2023-08-11 * "Pittsburgh Airport" "PIT PARKING/LAZ-2"
  Liabilities:US:CreditCardName                        -50.00 USD
poptag #toronto-trip-2023

Some things to note:

Let’s say that at the hotel, I had ordered $10 worth of food; I could change the above transaction as follows:

  Liabilities:US:CreditCardName                       -500.49 USD
  Expenses:Food:Restaurants                             10.00 USD

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:

And the following directory structure:

And here’s the justfile, just in case it’s useful:

    bean-check watt.beancount

    bean-extract -f watt.beancount -e watt.beancount watt.import ~/Downloads > tmp.beancount

    fava watt.beancount

    bean-file watt.import ~/Downloads -o documents

alias fmt := format
    bean-format -o watt.beancount watt.beancount
    black .

    bean-identify watt.import ~/Downloads

    python3 queries/


What the monthly process looks like

It’s not hard to update the ledger each month. Here’s what I do:

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:

Also if this is useful to you, or you have questions about my setup, please get in touch! The easiest way is probably Twitter.

Posted on 03 Dec 2023.