Introduction to Beancount 3
Experimental Documentation This repository is an experiment to verify if Artificial Intelligence can create high-quality technical documentation. The content you see here was generated by an AI agent.
Welcome to the Beancount 3 documentation.
Beancount is a double-entry bookkeeping system that runs on your computer. Unlike traditional accounting software (like Quicken, YNAB, or GnuCash) that stores data in complex databases or proprietary file formats, Beancount defines a simple language to describe financial transactions in plain text files.
You use your favorite text editor to write down what happened with your money, and Beancount provides the tools to read those files, verify that the math works out, and generate financial reports.
The Philosophy: Plain Text Accounting
Beancount belongs to the family of Plain Text Accounting (PTA) tools. This approach offers several unique advantages:
- You own your data: Your financial history is stored in text files that will be readable 50 years from now, regardless of whether Beancount still exists.
- Version Control: Because it’s text, you can manage your finances with Git. You can track changes, revert mistakes, and sync across devices securely.
- Speed and Efficiency: Entering data is as fast as you can type. There are no slow graphical interfaces or mouse clicks required to enter a coffee purchase.
- Programmability: Since the data is text, it is trivial to write scripts to manipulate it, generate custom charts, or import data from your bank.
Why Beancount?
Among the PTA tools (which includes Ledger and Hledger), Beancount is known for:
- Strictness: Beancount enforces rules (like requiring you to declare accounts before using them) that prevent typos and errors from creeping into your history.
- Python: Beancount is written in Python (with a C++ core in v3), making it incredibly extensible for anyone who knows the language.
- Inventory Booking: It has a robust model for tracking investments, handling stock lots, cost basis, and capital gains automatically.
How this guide is organized
This documentation follows the Diátaxis framework to help you find exactly what you need:
- Tutorials: Learning-oriented lessons. Start here if you are brand new to Beancount. We’ll walk you through your first file step-by-step.
- How-to Guides: Task-oriented recipes. Go here if you want to know “How do I track a stock trade?” or “How do I handle a shared expense?”.
- Reference: Information-oriented descriptions. Look here for the technical details of the syntax or CLI commands.
- Explanation: Understanding-oriented discussions. Read these to understand the concepts behind double-entry accounting and Beancount’s design decisions.
Getting Started with Beancount 3
This tutorial guides you through creating your first Beancount ledger. You will learn how to set up the file structure, define your accounts, and record your first transactions.
By the end of this tutorial, you will have a working accounting system that can produce a balance sheet.
Step 1: Installation Check
Before we begin, ensure you have Beancount 3 installed in your environment.
python3 -m beancount --version
If this command fails, please refer to the installation instructions for your operating system.
Step 2: The Options
Create a new empty text file named main.beancount. This will be your master ledger.
The first thing every Beancount file needs is some basic configuration. Add these lines to the top of your file:
option "title" "My Personal Finances"
option "operating_currency" "USD"
title: Gives your ledger a name, which appears in reports.operating_currency: Tells Beancount which currency is “yours.” You can have multiple operating currencies (e.g.,"USD", "EUR"), but you must have at least one.
Step 3: Defining Accounts
Unlike some other plain text accounting tools, Beancount requires you to declare an account before you use it. This strictness helps catch typos (like typing Assets:Chekcing by mistake).
We use the open directive to create accounts. We usually date these back to a time before we started tracking (like 1970 or 2000).
Tip: Not sure how to name your accounts? Check out the Organizing Accounts guide.
Add these lines to your file:
; Define the equity account for initial balances
2000-01-01 open Equity:Opening-Balances
; Define our main bank account
2000-01-01 open Assets:Checking:Chase
; Define a category for food expenses
2000-01-01 open Expenses:Food:Groceries
Step 4: Setting the Opening Balance
When you start tracking your finances, you likely already have money in your bank account. We need to record this.
In double-entry accounting, money cannot appear out of thin air. It must come from somewhere. For opening balances, it comes from Equity.
Let’s say you have $1,000 in your account on Jan 1st, 2024. Add this transaction:
2024-01-01 * "Opening Balance for Checking"
Assets:Checking:Chase 1000.00 USD
Equity:Opening-Balances -1000.00 USD
Notice that the two numbers sum to zero (+1000 and -1000). This is the core rule: Every transaction must balance to zero.
Step 5: Recording a Purchase
Now, let’s spend some money. On Jan 2nd, you bought groceries for $50.
2024-01-02 * "Whole Foods" "Weekly groceries"
Expenses:Food:Groceries 50.00 USD
Assets:Checking:Chase -50.00 USD
Learn More: See Recording Transactions for more complex examples like salary and split expenses.
- We increased our Expenses (Expenses are usually positive).
- We decreased our Assets (money left the checking account).
Step 6: Verification
Beancount comes with a powerful tool called bean-check. It reads your file and ensures:
- All syntax is correct.
- All transactions balance to zero.
- You haven’t used any undeclared accounts.
- Your balance assertions (if any) are correct.
Run this in your terminal:
bean-check main.beancount
Reference: For more details on command line tools, see the CLI Reference.
If the command runs silently (no output), your file is perfect! If there are errors, it will tell you the line number and the problem.
Step 7: Generating a Report
Now that we have data, let’s see a report. The bean-report tool can generate many views. Let’s look at the balances:
bean-report main.beancount balances
You should see output similar to:
Assets:Checking:Chase 950.00 USD
Equity:Opening-Balances -1000.00 USD
Expenses:Food:Groceries 50.00 USD
You now have a fully functional double-entry accounting system!
Recording Income and Split Transactions
In the Getting Started tutorial, we recorded a simple expense. Now, let’s tackle the other side of the equation: earning money.
We will also learn how to handle “Split Transactions”—transactions that touch more than two accounts. This is essential for recording things like your paycheck, which often has tax deductions, or a supermarket receipt that includes both food and household supplies.
Step 1: Defining New Accounts
Before we can record income, we need a place to put it. We also need accounts for the deductions we expect (like taxes).
Open your main.beancount file and add these open directives:
; Income accounts
2000-01-01 open Income:Salary
; Tax expenses
2000-01-01 open Expenses:Taxes:Federal
2000-01-01 open Expenses:Taxes:State
2000-01-01 open Expenses:Taxes:Medicare
Step 2: Understanding Income
In Beancount (and double-entry accounting in general), Income is recorded as a negative number.
This is often the most confusing part for beginners. Think of it this way:
- Assets are positive (Money you have).
- Income is negative (The source of that money).
When you get paid, money moves FROM your job (Income) TO your bank account (Asset).
Since the sum of a transaction must be zero:
(+Asset) + (-Income) = 0
Step 3: Recording a Simple Paycheck
Let’s start with a simplified example. You get paid ,000, deposited directly into your checking account.
2024-01-15 * "Employer Inc" "Salary"
Assets:Checking:Chase 2000.00 USD
Income:Salary -2000.00 USD
Run bean-check main.beancount. It should pass.
If you run bean-report main.beancount balances, you will see:
- Assets:Checking:Chase increased by ,000.
- Income:Salary is -2,000.
Step 4: The Real World (Split Transactions)
Real paychecks aren’t that simple. Usually, your “Gross Pay” is higher, but taxes and insurance are taken out before the money hits your bank.
Let’s say your Gross Pay is ,000.
- Federal Tax takes 00.
- State Tax takes 00.
- The remainder (,500) is deposited into your bank.
This is a Split Transaction. It involves one source (Income) and multiple destinations (Bank, Tax Expenses).
Add this transaction to your file:
2024-01-31 * "Employer Inc" "Salary with deductions"
Assets:Checking:Chase 2500.00 USD
Expenses:Taxes:Federal 400.00 USD
Expenses:Taxes:State 100.00 USD
Income:Salary -3000.00 USD
Why does this work?
Sum up the numbers:
+2500 (Bank) + 400 (Fed) + 100 (State) - 3000 (Income) = 0
The transaction balances. This accurately reflects reality: you earned ,000, but you only received ,500 in cash because you “spent” 00 on taxes immediately.
Step 5: Splitting Expenses
The same logic applies to spending money. Suppose you go to “SuperMart” and buy:
- Groceries (0)
- A new toaster (Household supplies, 0)
First, ensure you have the accounts:
2000-01-01 open Expenses:Household:Supplies
Now record the split expense:
2024-02-02 * "SuperMart" "Groceries and Toaster"
Expenses:Food:Groceries 60.00 USD
Expenses:Household:Supplies 30.00 USD
Assets:Checking:Chase -90.00 USD
Here, money leaves your Checking account (-90) and splits into two different expense buckets (+60 and +30).
Summary
- Income is negative. It balances out the positive increase in your Assets.
- Transactions can have many lines. As long as they sum to zero, you can split money across as many accounts as you need.
Next, we will look at how to handle debt and credit cards.
Managing Credit Cards and Loans
In this tutorial, we introduce a new type of account: Liabilities.
A Liability represents money you owe to someone else. The most common example is a Credit Card. Unlike a Debit card (which pulls money directly from your Assets), a Credit Card increases your debt, which you pay off later.
Step 1: Defining a Credit Card Account
Credit cards live under the Liabilities hierarchy. Let’s open an account for a Visa card.
Add this to your main.beancount:
2000-01-01 open Liabilities:CreditCard:Visa
Step 2: Buying with Credit
When you swipe your credit card, two things happen:
- You incur an Expense (e.g., you bought dinner).
- You incur a Liability (you now owe the bank money).
In Beancount, Liabilities are generally recorded as negative numbers, just like Income.
- Assets are positive (What you own).
- Liabilities are negative (What you owe).
However, unlike Income, we don’t usually talk about “increasing negative debt.” We just say the balance gets “more negative.”
Let’s record a 0 dinner on the Visa card:
2024-02-14 * "Romantic Bistro" "Valentine's Dinner"
Expenses:Food:Dining 50.00 USD
Liabilities:CreditCard:Visa -50.00 USD
Notice:
- Expense is Positive (+50).
- Liability is Negative (-50).
- Sum is Zero.
If you run bean-report main.beancount balances, your Visa account will show -50.00 USD. This means you owe 0.
Step 3: Buying More Stuff
Let’s buy something else. A 0 book.
2024-02-15 * "Bookstore" "New Novel"
Expenses:Entertainment:Books 20.00 USD
Liabilities:CreditCard:Visa -20.00 USD
(Make sure you open Expenses:Entertainment:Books if you haven’t already!)
Your Visa balance is now -70.00 USD.
Step 4: Paying the Bill
A month later, you receive your credit card statement. It says you owe 0. You pay it from your Checking account.
This transaction is a Transfer. You are moving money from an Asset to a Liability.
- Checking Account decreases (-70).
- Credit Card debt decreases (moves from -70 back towards 0, so +70).
2024-03-15 * "Chase Bank" "Paying off Visa bill"
Liabilities:CreditCard:Visa 70.00 USD
Assets:Checking:Chase -70.00 USD
After this transaction:
- Your Checking account has 0 less.
- Your Visa account balance is
0.00 USD. You are debt-free!
Common Confusion
New users often ask: “Why do I record the expense when I buy the item? Why not when I pay the bill?”
In double-entry accounting (and accrual accounting), you record the expense when it happens.
- Feb 14: You ate the dinner. That is when you “spent” the value.
- Mar 15: You moved cash to satisfy the debt. That is just a transfer of funds.
This gives you a more accurate picture of your spending. If you only recorded expenses when you paid the bill, it would look like you spent /run/current-system/sw/bin/bash in February and 0 on “Credit Card Bill” in March. That doesn’t tell you what you bought!
Summary
- Spending: Increase Expense (+), Increase Debt (-).
- Paying Bill: Decrease Debt (+), Decrease Asset (-).
- Liabilities usually have negative balances.
Next, we will learn how to make sure these numbers match your actual bank statements using Balance Assertions.
Reconciling with Balance Assertions
You have been entering transactions manually. But humans make mistakes. Maybe you typed 0.00 instead of .00, or you forgot to record that coffee you bought last Tuesday.
How do you trust your numbers?
In Beancount, we use Balance Assertions. This is the “killer feature” that makes plain-text accounting robust.
Step 1: What is a Balance Assertion?
A balance assertion is a statement of fact. You are telling Beancount: “I checked my bank website, and on this specific date, the balance was exactly X.”
Beancount will then calculate the balance of all your transactions up to that date.
- If they match: Great! Nothing happens.
- If they differ: Beancount stops with an error, telling you exactly how much you are off by.
Step 2: Adding an Assertion
Let’s say you log into your bank website on 2024-02-01. The website says your Checking account has ,450.00.
In your file, you might have entered transactions that leave you with ,450.00, or maybe ,445.00 if you missed a coffee.
Add this line to your file:
2024-02-01 balance Assets:Checking:Chase 1450.00 USD
Note the syntax:
- Date: The morning of this date (before any transactions on this date happen).
- Directive:
balance - Account: The account to check.
- Amount: The expected amount.
Step 3: Running the Check
Run the checker:
bean-check main.beancount
Scenario A: Success If the command outputs nothing (or just “no errors”), your history matches the bank’s reality perfectly.
Scenario B: Failure If you forgot a transaction, you might see an error like this:
main.beancount:45: Balance failed for Assets:Checking:Chase: expected 1450.00 USD but accumulated 1455.00 USD
This tells you:
- You expected ,450.
- Beancount calculated ,455 based on your transactions.
- You have too much in your Beancount file. This means you likely forgot to record a expense (or recorded a income twice).
Step 4: Finding the Error
When an assertion fails, you know the error happened between the last successful assertion and this one.
This is why frequent assertions are good.
- If you assert once a year, you have to search 365 days of transactions for the error.
- If you assert once a month (when your statement arrives), you only have to search one month.
To fix the error above ( difference), you would look through your bank statement, find the missing coffee, and add the transaction:
2024-01-28 * "Starbucks" "Forgot this one"
Expenses:Food:Coffee 5.00 USD
Assets:Checking:Chase -5.00 USD
Run bean-check again. The calculated balance will drop by , matching the assertion (450), and the error will disappear.
Best Practices
- Assert regularly: Every time you receive a monthly PDF statement from your bank, add a
balancedirective for the date of the statement. - Pad directives: Sometimes (especially when starting), you can’t find the error. Beancount has a
paddirective that auto-inserts a transaction to make the balance match. Use this sparingly! (See the Reference docs forpad). - Order matters: Assertions check the balance at the beginning of the day. If you have transactions on
2024-02-01, the assertion2024-02-01 balance ...checks the state before those transactions.
Summary
You have now completed the core tutorials!
- Basics: Created accounts and simple transactions.
- Income: Handled salary and split transactions.
- Liabilities: Managed credit card debt.
- Reconciliation: Used assertions to ensure accuracy.
You are ready to manage your own finances. Check the How-to Guides for specific scenarios like tracking investments, shared expenses, or multi-currency trips.
Advanced Investing: Stocks and Capital Gains
This tutorial moves beyond simple income and expenses to handle Assets held at Cost. This is the foundation of tracking investments, stocks, and currency trading in Beancount.
Step 1: Defining Accounts
Investment accounts look like regular asset accounts, but they hold Commodities (like “AAPL” or “GOOG”) instead of just Currency (USD).
Add these to your main.beancount:
2000-01-01 open Assets:Brokerage:Cash USD
2000-01-01 open Assets:Brokerage:Stocks AAPL, GOOG
2000-01-01 open Income:Dividends
2000-01-01 open Income:Capital-Gains
Step 2: Buying Stock (Cost Basis)
When you buy stock, you aren’t just transferring value; you are acquiring specific “lots” of an asset at a specific price. Beancount tracks this Cost Basis.
On Jan 1st, you buy 10 shares of AAPL at $150 each.
2024-01-01 * "Buy AAPL"
Assets:Brokerage:Stocks 10 AAPL {150.00 USD}
Assets:Brokerage:Cash -1500.00 USD
10 AAPL: The quantity and commodity.{150.00 USD}: The Cost per share. This attaches a “price tag” to these specific 10 shares.
Step 3: Buying More (Multiple Lots)
On Feb 1st, the price drops. You buy 5 more shares at $140.
2024-02-01 * "Buy more AAPL"
Assets:Brokerage:Stocks 5 AAPL {140.00 USD}
Assets:Brokerage:Cash -700.00 USD
You now own 15 shares of AAPL, but Beancount sees them as two distinct Lots:
- 10 shares @ $150
- 5 shares @ $140
Step 4: Selling and Realizing Gains
On March 1st, the price hits $160. You decide to sell 5 shares. But which 5 shares? The expensive ones ($150) or the cheap ones ($140)?
This is the Booking Method. By default, Beancount lets you choose specific lots (Specific Identification).
Let’s sell 5 shares from the first batch ($150).
2024-03-01 * "Sell AAPL"
Assets:Brokerage:Stocks -5 AAPL {150.00 USD} @ 160.00 USD
Assets:Brokerage:Cash 800.00 USD
Income:Capital-Gains -50.00 USD
Breaking it down:
-5 AAPL {150.00 USD}: We are removing 5 shares that had a cost of $150.@ 160.00 USD: We sold them at a market price of $160.Assets:Brokerage:Cash 800.00 USD: We received $800 cash (5 * $160).Income:Capital-Gains -50.00 USD: The profit.
The Math:
- Cost of sold shares: 5 * $150 = $750.
- Cash received: 5 * $160 = $800.
- Profit: $800 - $750 = $50.
Since Income is negative, we record -50.00 USD.
The transaction balances: -750 (Stock Cost) + 800 (Cash) - 50 (Profit) = 0.
Step 5: Dividends
Dividends are simply income.
2024-03-15 * "Apple Dividend"
Assets:Brokerage:Cash 15.00 USD
Income:Dividends -15.00 USD
Summary
- Use
{price}to attach cost basis when buying. - Use
{cost} @ pricewhen selling to calculate P/L. - Profit is the difference between the Cost Basis and the Sell Price.
How to Record Transactions
Transactions describe the movement of commodities (usually money) between accounts. While simple income and expense transactions are common, real life is often more complex.
This guide covers the standard patterns you will encounter.
The Anatomy of a Transaction
A transaction is a single unit that ensures money is neither created nor destroyed (except through Income/Equity).
2024-01-10 * "Payee" "Narration / Description"
Account1 -10.00 USD
Account2 10.00 USD
- Date:
YYYY-MM-DD. Strict requirement. - Flag:
*(cleared/confirmed) or!(pending). - Payee: (Optional) Who did you trade with?
- Narration: (Optional) What was this for?
- Postings: The lines indented below. You can have as many as you want, as long as they sum to zero.
1. Complex Income (Salary with Deductions)
Real paychecks are rarely just “Income -> Bank”. They include taxes, 401k contributions, and insurance. You record all of these in a single transaction.
Why is Income negative? In Beancount, Income is money flowing from the outside world into your accounts. To make the math balance to zero, Income is recorded as a negative number. Read the full explanation here.
2024-01-15 * "Tech Corp" "Payroll #42"
Assets:Checking:Chase 2500.00 USD ; Net pay deposited
Expenses:Taxes:Federal 500.00 USD
Expenses:Taxes:State 200.00 USD
Assets:401k:PreTax 300.00 USD ; Contribution to retirement
Expenses:Insurance:Health 100.00 USD
Income:Salary -3600.00 USD ; Gross Pay (negative)
Note that the Gross Pay (-3600) balances out all the positive numbers (2500+500+200+300+100).
2. Shared Expenses (Reimbursements)
If you pay for lunch for a friend and they pay you back later, you shouldn’t record the full amount as your expense.
Scenario: You pay $40. $20 is your food, $20 is for Alice.
2024-02-01 * "Restaurant" "Lunch with Alice"
Assets:Checking:Chase -40.00 USD
Expenses:Food:Dining 20.00 USD
Assets:Receivable:Alice 20.00 USD
Later, when Alice pays you back:
2024-02-05 * "Alice" "Venmo repayment"
Assets:Checking:Chase 20.00 USD
Assets:Receivable:Alice -20.00 USD
Your Assets:Receivable:Alice account is now zero.
3. Foreign Currency (Vacation)
When you use a card abroad, the bank converts the currency. You should record the actual cost to you (in your home currency) or the exact conversion if you want to track the foreign currency.
Simple approach (Convert at cost): You bought a €10 coffee, and your bank charged you $11.20.
2024-03-10 * "Cafe Paris" "Coffee"
Expenses:Travel:Food 11.20 USD
Assets:Checking:Chase -11.20 USD
Advanced approach (Tracking rates):
Explicitly stating the exchange rate using @.
2024-03-10 * "Cafe Paris" "Coffee"
Expenses:Travel:Food 10.00 EUR @ 1.12 USD
Assets:Checking:Chase -11.20 USD
4. Metadata, Tags, and Links
You can attach extra data to transactions to help with filtering later.
Deep Dive: For full syntax details, see the Syntax Reference.
- Tags: Use
#tagnamefor categories that cross-cut accounts (like#vacation2024or#tax-deductible). - Links: Use
^linknameto group related transactions (like an invoice and its payment). - Metadata: Key-value pairs for specific details.
2024-04-01 * "Hotel" "Stay in Tokyo" #vacation2024 ^invoice-1234
booking_id: "XYZ-999"
Liabilities:CreditCard -200.00 USD
Expenses:Travel:Lodging 200.00 USD
How to Manage Documents
Beancount allows you to link external files (PDFs, images) to specific transactions. This is invaluable for auditing and keeping track of receipts.
The document directive
To link a file to an account (without a specific transaction context), use the document directive.
2024-03-15 document Liabilities:CreditCard "/path/to/statement_mar2024.pdf"
Linking to Transactions
Beancount automatically associates documents with transactions if the date and tags match, but the most robust way is to use the metadata field statement or simply let Fava handle the discovery based on file location.
However, the standard convention in Beancount 3 is to use the document directive to declare the file exists, and then standard tooling (like Fava) displays it.
Organizing Files
Beancount enforces a specific directory structure for documents if you want automatic discovery:
/path/to/documents/
Liabilities/
CreditCard/
2024-03-15.Statement.pdf
- Set the
documentsoption in your file:option "documents" "/path/to/documents" - Beancount will scan this directory.
- Fava will automatically show
2024-03-15.Statement.pdfnext to any transaction involvingLiabilities:CreditCardon that date.
Best Practices
- Scans: Scan all physical receipts immediately.
- Naming: Use
YYYY-MM-DD.Description.pdfso they sort naturally in your file explorer. - Privacy: Remember these files are just on your disk. Beancount doesn’t encrypt them.
How to Organize Your Accounts
One of the first questions new users ask is “What accounts should I have?”
Beancount uses a hierarchical structure (separated by colons :) that allows you to be as general or specific as you want.
The Five Root Types (Strict Rule)
You cannot rename these. Every account must start with one of these five roots:
- Assets: Things you own that have value.
- Examples: Bank accounts, Cash, House, Car, Stocks, Receivables (money owed to you).
- Liabilities: Money you owe to others.
- Examples: Credit Cards, Mortgage, Student Loans.
- Income: Money flowing in to your life.
- Examples: Salary, Interest, Dividends, Gifts Received.
- Expenses: Money flowing out of your life (consumption).
- Examples: Rent, Food, Taxes, Utilities, Gifts Given.
- Equity: The net worth of the ledger.
- Examples: Opening balances, Conversions, Retained Earnings.
Example 1: Simple Personal Finance
For most people starting out, keep it simple. Don’t over-engineer.
Assets:US:BofA:Checking
Assets:US:BofA:Savings
Assets:Vanguard:401k
Liabilities:US:Chase:SapphireCard
Income:US:Employer:Salary
Income:US:Bank:Interest
Expenses:Home:Rent
Expenses:Home:Utilities
Expenses:Food:Groceries
Expenses:Food:Restaurant
Expenses:Transport:Metro
Expenses:Transport:Uber
Expenses:Shopping:Clothing
Expenses:Health:Medical
Example 2: The Freelancer
Freelancers need to separate business and personal expenses.
; Business Assets
Assets:Biz:PayPal
Assets:Biz:Checking
; Personal Assets
Assets:Pers:Checking
; Business Income
Income:Biz:ClientA
Income:Biz:ClientB
; Expenses
Expenses:Biz:Hosting
Expenses:Biz:Software
Expenses:Pers:Food
Tips for Success
1. Geography is useful
If you have accounts in multiple countries, put the country code early in the hierarchy:
Assets:US:ChaseAssets:UK:BarclaysThis groups all your US assets together in reports.
2. Institution Names
Using the bank name helps you match physical statements to Beancount accounts:
Liabilities:US:Amex:GoldLiabilities:US:Amex:Platinum
3. Don’t go too deep
Avoid: Expenses:Food:Groceries:Fruits:Bananas.
Better: Expenses:Food:Groceries.
Why? If you are too specific, classifying transactions becomes a chore. You can always use #tags for temporary specificity (like #wedding or #renovation).
4. Group by Fixed vs Variable
Some users like to separate fixed bills from discretionary spending:
Expenses:Fixed:RentExpenses:Fixed:InternetExpenses:Variable:DiningExpenses:Variable:Hobbies
Managing Shared Expenses
When you share finances with a partner, roommate, or group, you often pay for things on their behalf. Beancount handles this by tracking Receivables (money owed to you) and Payables (money you owe).
Scenario 1: You pay, they owe you
You pay $100 for dinner. $50 is for you, $50 is for your friend “Alice”.
2024-06-01 * "Restaurant" "Dinner with Alice"
Assets:CreditCard:Visa -100.00 USD
Expenses:Food:Dining 50.00 USD
Assets:Receivable:Alice 50.00 USD
- Your Expense: Only $50.
- Asset: You now hold a $50 claim against Alice.
When Alice pays you back:
2024-06-05 * "Alice" "Venmo repayment"
Assets:Bank:Checking 50.00 USD
Assets:Receivable:Alice -50.00 USD
The Receivable account is now zero.
Scenario 2: They pay, you owe them
Alice pays $100. Your share is $50.
2024-06-01 * "Alice" "Paid for dinner"
Expenses:Food:Dining 50.00 USD
Liabilities:Payable:Alice -50.00 USD
When you pay her back:
2024-06-05 * "Venmo" "Paying Alice back"
Liabilities:Payable:Alice 50.00 USD
Assets:Bank:Checking -50.00 USD
Scenario 3: Netting Out (The “Tabs” approach)
If you live with someone, you might not settle every transaction. You just want to track the running balance.
Use a single account: Assets:Partner.
- Positive balance: They owe you.
- Negative balance: You owe them.
You pay electric bill ($100, split 50/50):
2024-06-01 * "Electric Co"
Assets:Bank -100.00 USD
Expenses:Utilities 50.00 USD
Assets:Partner 50.00 USD
They pay rent ($2000, split 1000/1000):
2024-06-01 * "Partner paid rent"
Expenses:Housing:Rent 1000.00 USD
Assets:Partner -1000.00 USD
Current Balance of Assets:Partner: -950.00 USD. You owe them $950.
You write them a check for $950 to settle up.
Health Care & Insurance
Medical billing is complex. A single doctor’s visit can generate multiple bills, insurance adjustments, and reimbursements over months. Beancount’s Link feature (^) is perfect for this.
Tracking Claims (The “Incident”)
Use a unique link identifier for each medical event.
Step 1: The Visit You go to the doctor. You pay a $30 copay.
2024-03-01 * "Dr. Smith" "Checkup" ^claim-2024-03-smith
Expenses:Medical:Doctor 30.00 USD
Assets:CreditCard -30.00 USD
Step 2: The Bill Arrives A month later, you get a bill for the remaining $150 that insurance didn’t cover.
2024-04-01 * "Dr. Smith" "Remaining balance" ^claim-2024-03-smith
Expenses:Medical:Doctor 150.00 USD
Assets:CreditCard -150.00 USD
Step 3: Insurance Reimbursement Oops, insurance decided to cover $50 of that after all.
2024-04-15 * "Aetna" "Reimbursement" ^claim-2024-03-smith
Assets:Bank 50.00 USD
Expenses:Medical:Doctor -50.00 USD
By clicking the ^claim-2024-03-smith link in Fava, you can see the entire history of this specific medical event and calculate the true final cost ($130).
Health Savings Accounts (HSA)
HSAs are triple-tax-advantaged accounts. Treat them like any other investment asset.
Contribution (Payroll)
2024-01-15 * "Paycheck"
Assets:Bank 2000.00 USD
Assets:HSA:Cash 100.00 USD
Income:Salary -2100.00 USD
Spending from HSA When you use your HSA card, you are spending directly from that asset.
2024-03-01 * "Pharmacy" "Prescription"
Expenses:Medical:Meds 15.00 USD
Assets:HSA:Cash -15.00 USD
Investing HSA If you invest your HSA funds, track the commodities just like a brokerage account.
2024-06-01 * "Buy Fund"
Assets:HSA:VTSAX 5 VTSAX {100.00 USD}
Assets:HSA:Cash -500.00 USD
How to Record Trades
Tracking investments requires recording the cost basis (what you paid) so you can calculate capital gains when you sell.
Buying Assets
When you buy an asset (like a stock), you record the cost per share using curly braces {}.
Why the curly braces? Beancount tracks distinct “lots” of inventory. See How Inventories Work for a deep dive.
2024-05-01 * "Buy AAPL"
Assets:Brokerage:Cash -1500.00 USD
Assets:Brokerage:AAPL 10 AAPL {150.00 USD}
Here, we bought 10 shares of AAPL at $150.00 each.
Selling Assets
When you sell, you must specify which “lot” you are selling by matching the original cost basis. You also record the selling price using @.
The difference between the cost {...} and the price @ is your Capital Gain (or Loss).
2024-06-01 * "Sell AAPL"
Assets:Brokerage:AAPL -5 AAPL {150.00 USD} @ 160.00 USD
Assets:Brokerage:Cash 800.00 USD
Income:CapitalGains -50.00 USD
We sold 5 shares. Cost: $750 (5 * 150). Proceeds: $800 (5 * 160). Profit: $50.
Taxable vs. Tax-Exempt Accounts
The syntax for the trade is the same, but the Income account you use for the gains should differ.
1. Taxable Account
In a standard brokerage account, every sale is a taxable event. You should book gains to an Income account that reflects taxable income.
2024-06-01 * "Sell Stock in Taxable"
Assets:Taxable:AAPL -5 AAPL {150.00 USD} @ 160.00 USD
Assets:Taxable:Cash 800.00 USD
Income:Taxable:CapitalGains -50.00 USD
2. Tax-Exempt / Tax-Advantaged Account (IRA, 401k)
In accounts like an IRA, you don’t pay taxes on individual trades. However, Beancount still requires the transaction to balance. You still made a profit, even if the IRS doesn’t care yet.
To handle this, map the gains to an Income account specifically for that tax shelter. This keeps it separate from your taxable income reports.
2024-06-01 * "Sell Stock in IRA"
Assets:IRA:AAPL -5 AAPL {150.00 USD} @ 160.00 USD
Assets:IRA:Cash 800.00 USD
Income:IRA:Gains -50.00 USD
By separating Income:Taxable:CapitalGains and Income:IRA:Gains, you can easily filter your reports to see only your taxable obligations.
How to Track Stock Vesting (RSUs)
Restricted Stock Units (RSUs) are a common form of compensation. Tracking them requires handling the income event (vesting) and the subsequent capital gains (selling).
1. The Grant
When you are granted RSUs, you don’t actually own anything yet. You technically don’t need to record this in your main ledger, but you can track it in a separate “Shadow” account if you wish.
Most users skip this step and start at Vesting.
2. Vesting (The Taxable Event)
When shares vest, two things happen:
- You receive shares (Asset).
- You pay taxes (Expense), usually by selling some of those shares immediately (“Sell to Cover”).
- You recognize the total value as Income.
Example:
- You vest 10 shares of
GOOG. - Market price is $100/share. Total Value: $1000.
- Tax rate is 40%. You “sell” 4 shares to pay $400 in taxes.
- You keep 6 shares.
2024-05-15 * "Employer" "RSU Vest - 10 shares"
Income:Employer:RSU -1000.00 USD ; Total Grant Value
Expenses:Taxes:Federal 400.00 USD ; Taxes paid
Assets:Brokerage:GOOG 6 GOOG {100.00 USD}
- Income: We record the full gross amount ($1000).
- Asset: We record the 6 shares we actually kept, with a cost basis of the market price at that moment (
{100.00 USD}).
3. Selling
Later, when you sell the shares, you calculate Capital Gains based on that cost basis.
Scenario: Price rises to $120. You sell your 6 shares.
2024-08-01 * "Broker" "Sell RSUs"
Assets:Brokerage:GOOG -6 GOOG {100.00 USD} @ 120.00 USD
Assets:Brokerage:Cash 720.00 USD
Income:CapitalGains -120.00 USD
- Cost: $600 (6 * 100).
- Proceeds: $720 (6 * 120).
- Gain: $120.
Employee Stock Purchase Plan (ESPP)
ESPP is different. You buy shares at a discount.
Example:
- Market Price: $100.
- Discount: 15% (You pay $85).
- You buy 10 shares.
2024-06-30 * "Employer" "ESPP Purchase"
Assets:Brokerage:Cash -850.00 USD ; You paid $850
Income:Employer:ESPP-Discount -150.00 USD ; The discount is essentially income
Assets:Brokerage:GOOG 10 GOOG {100.00 USD} ; Cost basis is Fair Market Value
Note: ESPP tax rules are complex (Qualifying vs Disqualifying dispositions). This method sets the cost basis to the FMV ($100), treating the discount as immediate income. Consult a tax professional for your specific reporting needs.
How to Track Settlement Dates
In financial trading, there is often a delay between the Trade Date (when you agreed to the deal) and the Settlement Date (when the cash actually moves).
- Trade Date: You bought the stock today. You own it now.
- Settlement Date: The cash leaves your account in 2 days (T+2).
Beancount generally uses a single date per transaction. This guide shows you how to track both using an intermediate “Pending” account.
The Strategy
Instead of moving cash directly from Assets:Checking to Assets:Stock, we move it through Assets:Pending:Transfers.
- On Trade Date: Record the stock purchase and a debt to the pending account.
- On Settlement Date: Record the cash movement clearing the debt.
Example: Buying Stock
You buy 10 shares of AAPL for $150 on June 1st. The cash will settle on June 3rd.
Step 1: The Trade (June 1)
You legally own the stock now. You “owe” the cash to the settlement clearing house.
2024-06-01 * "Buy AAPL"
Assets:Brokerage:AAPL 10 AAPL {150.00 USD}
Assets:Pending:Transfers -1500.00 USD
- Stock: Account increases (You own it).
- Pending: Account decreases (You owe money).
Step 2: The Settlement (June 3)
The bank actually transfers the money.
2024-06-03 * "Settlement for AAPL"
Assets:Pending:Transfers 1500.00 USD
Assets:Checking -1500.00 USD
- Pending: Account returns to zero.
- Checking: Cash leaves your bank.
Using the pad Directive
If you import your bank data automatically, your bank will only show the transaction on June 3rd. Your brokerage might show the trade on June 1st.
You can use pad to fill the gap automatically without manually writing the settlement transaction every time.
-
Trade (From Brokerage Importer):
2024-06-01 * "Buy AAPL" Assets:Brokerage:AAPL 10 AAPL {150.00 USD} Assets:Pending:Transfers(Note: We leave the second leg empty or let the importer calculate it)
-
Cash (From Bank Importer):
2024-06-03 * "ACH Transfer" Assets:Checking -1500.00 USD Assets:Pending:Transfers -
Pad Directive: If you don’t have the exact matching transaction, you can tell Beancount to “fill the gap” in the Pending account.
2024-06-02 pad Assets:Pending:Transfers Equity:Timing-DifferencesUse
padwith caution! It effectively “invents” a transaction to make balances match.
Benefits
- Accuracy: Your stock holdings are correct on the trade date (important for dividends and voting rights).
- Reconciliation: Your bank balance matches the bank statement dates (settlement dates).
Portfolio Returns
A common question is: “What is my investment return?” (e.g., “I made 8% this year”).
Beancount does not calculate percentage returns (IRR or TWR) natively in the core engine. It focuses on accounting correctness (Cost Basis and Capital Gains). However, you can extract the data needed to calculate this.
The Challenge
To calculate returns, you need to know:
- The value of the portfolio at the start.
- The value at the end.
- The exact date and amount of every cash flow in and out.
Since Beancount tracks all of this, the data is there.
Solution 1: Fava
Fava has a built-in “Holdings” view that attempts to calculate returns for your assets. This is the easiest way for most users.
Solution 2: Scripts
The standard way to calculate returns in the Beancount ecosystem is to use a script to extract the cash flows and compute the Internal Rate of Return (IRR).
Beancount provides a helper library beancount.plugins.ira_contribs which can help track contributions, but for returns, community scripts are often used.
Calculating Simple Gain (Profit)
You can easily see your total profit (Market Value - Cost Basis) in bean-query.
SELECT
account,
sum(convert(position, 'USD')) as market_value,
sum(cost(position)) as cost_basis,
(sum(convert(position, 'USD')) - sum(cost(position))) as unrealized_gain
WHERE account ~ "Assets:Investments"
GROUP BY account
Note: This query requires that you have updated prices for your commodities.
Short Sales
A Short Sale is when you sell an asset you don’t own (by borrowing it), hoping to buy it back later at a lower price.
In Beancount terms, this means holding a negative number of units held at cost.
The Constraint
By default, Beancount enforces a constraint: you cannot have a negative inventory of lots.
- If you hold
10 GOOG {100 USD}, you can sell at most 10. - If you try to sell 15, you get an error.
Enabling Short Sales
To allow short selling, you generally need to relax this constraint or use a dedicated account where the negative balance is expected.
Method 1: Dedicated Short Account
Separate your “Long” and “Short” positions.
2024-06-01 * "Short Sell"
Assets:Brokerage:Cash 1000.00 USD
Liabilities:Brokerage:Shorts -10 GOOG {100.00 USD}
Here, we use a Liability account to track the short position. A negative liability is… well, usually a liability is already negative (credit). Wait.
In Beancount:
- Assets are Positive.
- Liabilities are Negative.
If you are Short, you owe the stock. It is a Liability.
So Liabilities:Brokerage:Shorts should have a negative number of units.
Closing the Short (Covering): You buy the stock back.
2024-07-01 * "Cover Short"
Liabilities:Brokerage:Shorts 10 GOOG {100.00 USD} @ 90.00 USD
Assets:Brokerage:Cash -900.00 USD
Income:CapitalGains -100.00 USD
- You “buy” 10 units to neutralize the -10 units.
- Cost Basis match:
{100.00 USD}. - Price paid:
$900. - Profit:
$100.
Caveats
- Dividends: If you are short, you pay dividends instead of receiving them. This is an Expense.
- Interest: You usually pay margin interest on the borrowed value.
How to Fetch Market Prices
To track the value of your investments (Stocks, ETFs, Crypto) in Beancount, you need to record their market price over time.
The price Directive
Manually, you can add prices like this:
2024-01-01 price AAPL 150.00 USD
2024-01-02 price AAPL 152.00 USD
But doing this manually is tedious.
Using bean-price
Beancount comes with a tool called bean-price that fetches prices from the internet.
1. Install Requirements
You might need to install extra sources:
pip install beancount-beanprice
2. Configure Your Commodities
You need to tell bean-price where to look for the price. You do this by adding metadata to your commodity directive.
Note: The
commoditydirective is optional in basic Beancount but required if you want to attach metadata like this. See Syntax Reference.
1980-01-01 commodity AAPL
price: "Yahoo/AAPL"
name: "Apple Inc."
1980-01-01 commodity BTC
price: "Coinbase/BTC-USD"
3. Run the Tool
Run bean-price against your ledger file. It will:
- Find all commodities with a
pricesource. - Find the last date they were updated.
- Fetch the current price.
bean-price main.beancount
4. Save the Prices
Usually, you append the output to a separate prices file (e.g., prices.beancount) that you include in your main file.
In main.beancount:
include "prices.beancount"
Run command:
bean-price -e USD main.beancount >> prices.beancount
How to Run Reports
Beancount is a command-line tool. While you can use GUIs like Fava (a separate web interface for Beancount), understanding the CLI tools gives you the most power.
The Main Command: bean-report
The basic syntax is:
bean-report [filename] [report-name]
1. balances (The Snapshot)
Shows the final balance of every account at the end of history.
bean-report main.beancount balances
Common Flags:
--at=2023-01-01: See what the balances were on a specific date.
2. income (The P&L)
Shows your profit and loss (Income and Expenses) for the period. By default, it calculates the total for the entire history of the file.
bean-report main.beancount income
Common Flags:
--begin=2023-01-01: Start calculating from this date.--end=2024-01-01: Stop calculating at this date.
3. check
Validates the file (same as bean-check).
bean-report main.beancount check
The SQL Interface: bean-query
For custom reports, Beancount provides a SQL-like query language. This is incredibly powerful.
Start the interactive shell:
bean-query main.beancount
Then type your query:
Example 1: Find all coffee purchases
SELECT date, narration, amount
WHERE account ~ "Expenses:Food" AND narration ~ "Coffee"
Example 2: Monthly spending on Food
SELECT year(date), month(date), sum(position)
WHERE account ~ "Expenses:Food"
GROUP BY year(date), month(date)
Example 3: Export to CSV You can run a query directly from the command line and save it to a CSV file for Excel/Google Sheets.
bean-query main.beancount 'HBOX "SELECT * WHERE account ~ 'Assets' "' -f csv > assets.csv
Visualizing with Fava
While not part of the core Beancount tool, almost every user installs Fava. It is a web interface that runs locally.
For a detailed guide on using its charts and filters, see Using Fava (Web UI).
Query Cookbook
bean-query is a powerful command-line tool that lets you extract data from your Beancount file using a SQL-like language called BQL (Beancount Query Language).
Unlike standard SQL which queries a database on disk, bean-query loads your text file into memory and runs queries against the internal data structures.
Note: For simple text reports (like balances or income statements), see Running Reports instead.
How to use bean-query
1. Interactive Mode (Shell)
If you run the command without a query string, you enter an interactive shell. This is best for exploring data.
$ bean-query main.beancount
Input file: "main.beancount"
Ready with 1000 directives (1500 postings).
beancount> SELECT sum(position) WHERE account ~ "Assets"
...
Type help inside the shell to see available commands.
2. Command Line Mode (Scripting)
You can pass the query directly as a string. This is useful for scripts or quick checks.
bean-query main.beancount "SELECT sum(position) WHERE account ~ 'Assets'"
Core Concepts
Available Columns
Every transaction posting has specific columns you can query:
date: The date of the transaction.account: The account name (e.g.,Assets:Bank).narration: The description text.payee: The payee text.position: The amount and currency (e.g.,10.00 USD).balance: The cumulative balance after this posting.tags: A set of tags attached to the transaction.links: A set of links attached to the transaction.
Functions
BQL supports many functions to manipulate data:
year(date),month(date): Extract date parts.root(account, 2): Get the parent account (e.g.,Assets:BankfromAssets:Bank:Chase).sum(position): Add up amounts (handles multiple currencies automatically).abs(amount): Absolute value.
Spending Analysis
How much did I spend on Coffee this year?
SELECT sum(position)
WHERE account ~ "Expenses:Food:Coffee"
AND year(date) = 2024
What are my top 10 expense categories?
SELECT account, sum(position) as total
WHERE account ~ "Expenses"
AND year(date) = 2024
GROUP BY account
ORDER BY total DESC
LIMIT 10
Income Analysis
Monthly Income by Source
SELECT year(date), month(date), root(account, 3), sum(position)
WHERE account ~ "Income"
GROUP BY year(date), month(date), root(account, 3)
ORDER BY year(date), month(date)
Net Worth
Current Net Worth (Sum of Assets + Liabilities) Note: Since Liabilities are negative, summing them gives the net value.
SELECT sum(position)
WHERE account ~ "Assets|Liabilities"
Auditing
Find huge transactions (over $1000)
SELECT date, narration, account, position
WHERE abs(number(position)) > 1000
ORDER BY date DESC
Find transactions missing a tag
SELECT *
WHERE tag IS NULL
Year-End Procedures
Beancount uses Continuous Accounting, which means you do not need to close out your books at the end of the year like a traditional business. Your Income and Expense accounts just keep accumulating history.
However, many users like to perform a “Year-End Check” to lock in their history.
1. Verify Balances
On Dec 31st (or Jan 1st), log into all your banks and check the exact balance. Add a balance assertion for every asset and liability account.
2024-01-01 balance Assets:Bank:Checking 1234.56 USD
2024-01-01 balance Liabilities:Visa -450.00 USD
This ensures that no future edits can accidentally break your history for 2023.
2. Generate Reports
Since you don’t zero out accounts, you must filter by date to see your annual performance.
# Income Statement for 2023
bean-report main.beancount income --year=2023
3. (Optional) “Clear” Equity
If you really want to zero out your Income/Expense accounts (simulating a “closing”), you can transfer their balances to Equity:Retained-Earnings.
Note: This is manual and usually discouraged in Beancount because it destroys the ability to run multi-year reports easily.
4. Archive Documents
If you store receipts, now is a good time to zip up the 2023/ folder.
Using Fava (Web UI)
Fava is the standard web interface for Beancount. It transforms your text file into an interactive financial dashboard.
Installation & Start
pip install fava
fava main.beancount
Open http://localhost:5000 in your browser.
Tip: Press
?anywhere in Fava to see keyboard shortcuts.
The Dashboard (Indicators)
The dashboard shows your Net Worth and upcoming events.
Up-to-Date Indicators
Fava can show colored dots next to accounts to indicate if they have been reconciled recently.
To enable this, add metadata to your open directive:
2000-01-01 open Assets:Bank:Chase
fava-uptodate-indication: TRUE
- 🟢 Green: The last entry is a passed balance check.
- 🔴 Red: The last entry is a failed balance check.
- 🟡 Yellow: The last entry is not a balance check.
- ⚪ Grey: No activity for a long time (default 60 days).
Filtering
Fava’s filter bar is extremely powerful.
Time Filter
Supports natural language and intervals:
2024: The whole year.2024-Q1: First quarter.2024-03: March.year - 1: Last year.month: This month.
Account Filter
Accepts regular expressions to match account names:
Assets:USmatches all US assets.^Assetsmatches only Assets (no Income/Expenses).
Advanced Filter
Filter by tags, links, metadata, or amounts.
#vacation: Only show transactions with this tag.payee:"Starbucks": Filter by payee.units > 100: Only transactions larger than 100.any(account:"Expenses:Food"): Entries where at least one posting hits food.all(-account:"Expenses"): Exclude all expenses.
The Editor
Fava includes a full Beancount text editor in the browser.
- Auto-complete: Type an account name or payee to see suggestions.
- Validation: Errors are highlighted in red immediately.
- Formatting: Use
Align Amountsto clean up your file.
Importing Data
Fava provides a GUI for beangulp importers.
- Configure your importers in python (see Writing Importers).
- Add the configuration to your file:
custom "fava-option" "import-config" "my_import_config.py" custom "fava-option" "import-dirs" "/path/to/downloads" - Go to the Import tab in Fava.
- Drag and drop CSV files onto the importer name.
- Fava will extract transactions and let you review/accept them before writing to your file.
Configuration Options
Fava has its own set of options defined using the custom directive.
Fiscal Year
Change the start of your year (e.g., UK tax year starts April 6).
custom "fava-option" "fiscal-year-end" "04-05"
Invert Signs
If you prefer to see Income/Liabilities as positive numbers (Net Profit view):
custom "fava-option" "invert-income-liabilities-equity" "true"
Language
Force a specific language (default is auto-detected).
custom "fava-option" "language" "fr"
Writing Importers with Beangulp
Beancount 3 uses a separate library called beangulp for importing data. Beangulp provides a framework to extract transactions from external files (like CSVs or PDFs) and convert them into Beancount directives.
Prerequisites
You need to install beangulp:
pip install beangulp
The Importer Protocol
To create a custom importer, you must subclass beangulp.Importer and implement these methods:
identify(self, filepath): ReturnsTrueif this importer can handle the given file.extract(self, filepath, existing): Parses the file and returns a list of Beancount entries.account(self, filepath): Returns the account associated with this file (used for filing).date(self, filepath): Returns the date associated with this file (used for filing).
Example: A Simple CSV Importer
Let’s say you have a bank CSV that looks like this:
Date,Description,Amount
2024-01-15,Coffee Shop,-5.00
2024-01-16,Salary,3000.00
Here is a complete importer for this format.
import csv
from datetime import datetime
from beangulp import Importer
from beancount.core import data, amount
from decimal import Decimal
class MyBankImporter(Importer):
def __init__(self, account_name):
self.account_name = account_name
def identify(self, filepath):
# Only accept CSV files
if not filepath.endswith(".csv"):
return False
# Peek at the header to be sure
with open(filepath) as f:
header = f.readline().strip()
return header == "Date,Description,Amount"
def account(self, filepath):
return self.account_name
def date(self, filepath):
# Optional: Extract the date from the filename or the last transaction
# For simplicity, we'll return the date of the last transaction in the file
last_date = None
with open(filepath) as f:
reader = csv.DictReader(f)
for row in reader:
last_date = datetime.strptime(row['Date'], '%Y-%m-%d').date()
return last_date
def extract(self, filepath, existing=None):
entries = []
with open(filepath) as f:
reader = csv.DictReader(f)
for row in reader:
# 1. Parse metadata
date = datetime.strptime(row['Date'], '%Y-%m-%d').date()
desc = row['Description']
amt = Decimal(row['Amount'])
# 2. Create the transaction
meta = data.new_metadata(filepath, reader.line_num)
txn = data.Transaction(
meta, date, "*", None, desc, data.EMPTY_SET, data.EMPTY_SET, []
)
# 3. Add postings
# The main posting (the bank account)
txn.postings.append(
data.Posting(self.account_name, amount.Amount(amt, "USD"),
None, None, None, None)
)
# The other side (usually an expense placeholder)
# We leave the amount empty so Beancount can balance it automatically
txn.postings.append(
data.Posting("Expenses:Unknown", None,
None, None, None, None)
)
entries.append(txn)
return entries
Extracting Data from PDFs
Sometimes banks only provide PDF statements. Extracting data from them is harder but possible using tools like pdftotext.
1. Install Dependencies
You need the poppler-utils package installed on your system.
- Debian/Ubuntu:
sudo apt install poppler-utils - macOS:
brew install poppler - Nix:
nix-env -iA nixpkgs.poppler_utils
2. Calling pdftotext
Use Python’s subprocess to run the command and capture the output. The -layout flag is crucial as it preserves the visual structure of the table.
import subprocess
def extract_pdf_text(filepath):
# Run pdftotext -layout input.pdf -
# The '-' at the end tells it to print to stdout
result = subprocess.run(
['pdftotext', '-layout', filepath, '-'],
stdout=subprocess.PIPE,
text=True
)
return result.stdout
3. Parsing the Output
Once you have the text string, you iterate over the lines and use Regular Expressions (Regex) to find transaction rows.
import re
# Example Regex for a line like: "Jan 01 Grocery Store -50.00"
# (\w+ \d+) : Date (Group 1)
# \s+ : Spaces
# (.*?) : Description (Group 2)
# \s+ : Spaces
# (-?[\d\.]+) : Amount (Group 3)
TXN_REGEX = re.compile(r'(\w+ \d+)\s+(.*?)\s+(-?[\d\.]+)')
def parse_text(text):
for line in text.splitlines():
match = TXN_REGEX.search(line)
if match:
date_str, desc, amount_str = match.groups()
# ... create transaction object ...
Running Your Import
Create a configuration script (e.g., import.py) to run your importers.
from beangulp import Ingest
import my_importers
ingest = Ingest()
ingest.add(my_importers.MyBankImporter("Assets:Bank:Checking"))
if __name__ == "__main__":
ingest()
You can now run this script to generate beancount entries:
python3 import.py /path/to/downloaded.csv
Best Practices
- Use
beangulp.testing: Beangulp includes utilities to test your importers against sample files. - Smart Categorization: You can add logic in
extractto guess the expense account based on the description (e.g., if “Coffee” in description, useExpenses:Food:Coffee).
Beancount 3 Syntax Reference
Beancount uses a precise but simple text format.
General Rules
- Indentation: Postings must be indented (usually 2 spaces). Directives must start at the beginning of the line.
- Comments: Any text following a
;is ignored. - Strings: Payees and Narrations must be in double quotes
".
Directives
1. option
Configures global settings.
option "title" "My Finances"
2. plugin
Loads Python extensions.
plugin "beancount.plugins.auto_accounts"
3. open
Initializes an account. Usually dated before the first transaction.
2000-01-01 open Assets:Bank:Chase
You can also restrict the currencies allowed in an account:
2000-01-01 open Assets:Bank:Chase USD, EUR
Syntax: open Account [Constraint1, Constraint2] ["BookingMethod"]
4. close
Marks an account as closed. The balance must be zero on this date.
2024-12-31 close Assets:Bank:OldAccount
5. commodity
Declares a currency or commodity. Optional, but useful for metadata.
1998-01-01 commodity USD
name: "US Dollar"
6. price
Records the market price of a commodity on a specific date.
2024-06-01 price AAPL 155.00 USD
7. balance
Asserts that the balance of an account matches a number at the end of that day.
2024-01-01 balance Assets:Bank:Chase 1500.00 USD
8. pad
Automatically inserts a transaction to make the balance of an account match a future balance directive.
The difference is booked to the source account provided.
; Calculate the difference needed to reach the next balance check
; and book it to Equity:Opening-Balances
2024-01-01 pad Assets:Bank:Chase Equity:Opening-Balances
2024-01-02 balance Assets:Bank:Chase 1500.00 USD
Use with caution! It effectively hides missing transactions.
9. custom
Used to define generic directives that can be handled by scripts or plugins.
2024-01-01 custom "budget" "Expenses:Food" "monthly" 500.00 USD
The Transaction Block
The most common block in the file.
Examples: See Recording Transactions for practical examples.
YYYY-MM-DD flag "Payee" "Narration" #tag ^link
meta-key: meta-value
Account1 Amount1 Currency
Account2 Amount2 Currency
- Flag:
*(Completed) or!(Incomplete). - Payee/Narration: At least one string is required. Beancount assumes the first string is the payee if there are two strings, or just the narration if there is only one.
- Tags:
#tagname. Used for categorization. - Links:
^linkname. Used to group transactions. - Metadata: Key-value pairs. Values can be strings, numbers, dates, or booleans.
Balancing Rules
Beancount enforces that every transaction sums to zero.
- Amount Only: Weight is simply the amount (e.g.,
10 USD). - Cost: Weight is
Amount * Cost(e.g.,10 STOCK {100 USD}=1000 USD). - Price: Weight is
Amount * Price(e.g.,10 EUR @ 1.2 USD=12 USD). - Cost & Price: Price is ignored for balancing. Weight is
Amount * Cost.
Cost Basis {} and Price @
Used for trading and conversions.
{10.00 USD}: Cost. I bought this item for 10 USD.@ 12.00 USD: Price. The market value of this item is 12 USD per unit.@@ 120.00 USD: Total Price. The total value of the entire lot is 120 USD.
Example: Buying 10 shares at $10 each.
2024-01-01 * "Buy Stock"
Assets:Brokerage 10 STOCK {10.00 USD}
Assets:Cash -100.00 USD
Options Reference
Global options configure how Beancount parses and validates your file.
Usage:
option "key" "value"
Essential Options
title
The title of your ledger, used in reports.
option "title" "My Personal Finances"
operating_currency
Defines the currencies that are “yours”. This affects how reports aggregate numbers. You can specify this multiple times.
option "operating_currency" "USD"
option "operating_currency" "EUR"
Parsing & Accuracy
tolerance
Sets the inferred tolerance for balancing transactions. Defaults to 0.005 for most currencies.
option "tolerance" "0.01"
account_rounding
Automatically inserts a rounding posting to an Equity account if a transaction is off by a tiny amount (due to currency conversion).
option "account_rounding" "Equity:Rounding"
Classification
documents
A list of directories where Beancount should look for external documents (PDFs) to link to transactions.
option "documents" "/home/user/finances/docs"
account_previous_balances
Beancount can calculate the balance of an account before the first transaction based on open date.
option "account_previous_balances" "Opening-Balances"
Formatting
render_commas
Whether to render numbers with thousands separators in reports (TRUE/FALSE).
option "render_commas" "TRUE"
plugin_processing_mode
“default” or “raw”. Determines when plugins run. usually “default”.
option "plugin_processing_mode" "default"
Root Account Naming (Localization)
You can rename the five standard root accounts to suit your language or preference.
name_assets, name_liabilities, name_equity, name_income, name_expenses
Override the default names.
option "name_assets" "Actifs"
option "name_liabilities" "Passifs"
option "name_equity" "Capitaux"
option "name_income" "Revenus"
option "name_expenses" "Depenses"
Beancount Query Language (BQL)
BQL is a SQL-like query language designed specifically for filtering and aggregating double-entry accounting data. It runs on the in-memory data structures of Beancount.
Query Structure
The basic syntax mirrors SQL:
SELECT <target1>, <target2>, ...
FROM <entry-filter>
WHERE <posting-filter>
GROUP BY <grouping1>, <grouping2>, ...
ORDER BY <order1>, <order2>, ...
LIMIT <number>
1. Data Sources (FROM)
The FROM clause selects which Entries (transactions) are considered. If an entry is excluded here, none of its postings will be visible to the rest of the query.
#tagname: Select entries with a specific tag.^linkname: Select entries with a specific link.has_tag('tagname'): Standard function form.has_link('linkname'): Standard function form.RE("pattern"): Match narration or payee against a regex.
Example:
SELECT * FROM has_tag('vacation2024')
2. Filtering Postings (WHERE)
The WHERE clause filters individual Postings within the selected entries.
account: The account name (string).currency: The commodity currency (string).date: The date of the transaction.year,month,day: Date components.meta(key): Access metadata values.
Operators:
=,!=: Equality.<,<=,>,>=: Comparison.~: Regular expression match (case-insensitive).AND,OR,NOT: Logical operators.
Example:
SELECT * WHERE account ~ "Expenses:Food" AND currency = "USD"
3. Columns and Functions (SELECT)
Data Columns
These are the raw attributes available for every posting:
| Column | Type | Description |
|---|---|---|
date | Date | Date of the transaction. |
flag | String | Transaction flag (* or !). |
account | String | The account name. |
payee | String | The payee name. |
narration | String | The transaction description. |
position | Position | The held amount (units + cost). |
balance | Position | Running balance (only in journal mode). |
id | ID | Unique internal ID. |
Simple Functions
These functions operate on a single row/posting.
| Function | Description | Example |
|---|---|---|
COST(pos) | Get the cost inventory of a position. | COST(position) |
UNITS(pos) | Get the number of units. | UNITS(position) |
CURRENCY(pos) | Get the currency of the position. | CURRENCY(position) |
ABS(num) | Absolute value. | ABS(number) |
NEG(num) | Negate value. | NEG(number) |
PARENT(account) | Get parent account name. | PARENT(account) |
ROOT(account, n) | Get the n-th root component. | ROOT(account, 1) |
JOIN(str, ...) | Join strings. | JOIN(payee, narration) |
META(key) | Get metadata value. | META('booking_id') |
TODAY() | Returns current date. | date > TODAY() |
Date Functions
| Function | Description |
|---|---|
YEAR(date) | Extract year (integer). |
MONTH(date) | Extract month (integer). |
DAY(date) | Extract day (integer). |
QUARTER(date) | Format as “2024Q1”. |
Aggregate Functions
These functions require a GROUP BY clause.
| Function | Description |
|---|---|
SUM(pos) | Sums positions. Handles mixed currencies automatically. |
COUNT(val) | Counts number of rows. |
MIN(val) | Minimum value. |
MAX(val) | Maximum value. |
FIRST(val) | First value in the group sequence. |
LAST(val) | Last value in the group sequence. |
4. Interactive Command Shell
You can run BQL queries interactively using the bean-query tool.
bean-query main.beancount
Inside the shell:
help: List available commands.schema: Show available columns.targets: Show available functions.
Common Recipes
Monthly Expenses by Category:
SELECT
year(date) as year,
month(date) as month,
root(account, 2) as category,
SUM(position)
WHERE
account ~ "Expenses"
GROUP BY year, month, category
ORDER BY year, month, category
Last Reconciled Date per Account:
SELECT
account,
MAX(date) as last_reconciled
WHERE
flag = "*"
GROUP BY account
Standard Plugins
Beancount includes several plugins that automate common tasks. To use them, add plugin "..." to your file.
beancount.plugins.auto_accounts
Description: Automatically creates open directives for accounts that are referenced but not defined.
Usage: Great for lazy prototyping, but discouraged for strict accounting.
plugin "beancount.plugins.auto_accounts"
beancount.plugins.tag_pending
Description: Automatically adds a #pending tag to any transaction marked with the ! flag.
Usage: clear visualization of what is not yet cleared.
plugin "beancount.plugins.tag_pending"
beancount.plugins.forecast
Description: Generates recurring transactions into the future for budgeting/planning. Usage: You define a “forecast” transaction, and the plugin projects it forward.
beancount.plugins.check_commodity
Description: Ensures that all commodities used in an account match the open directive restrictions.
Usage: Strict validation.
2000-01-01 open Assets:Bank:US USD
; This will fail if you try to put EUR in it
Beancount 3 Command Line Interface
Beancount 3 provides several tools to interact with your ledger files.
bean-check
This is the most important command. It checks your file for syntax errors and balance mistakes.
bean-check my_ledger.beancount
If there are no errors, the command will produce no output.
bean-report
Generates reports from your data. Common reports include:
balances: Shows the balance of all accounts.income: Shows your income and expenses.
bean-report my_ledger.beancount balances
bean-query
Allows you to run SQL-like queries on your transactions. This is very powerful for custom analysis.
bean-query my_ledger.beancount "SELECT date, narration, amount WHERE account ~ 'Expenses'"
bean-format
Automatically cleans up your ledger file by aligning amounts and sorting directives where possible.
bean-format my_ledger.beancount
Note: Beancount 3 focuses on performance and correctness. Always ensure you are running the version compatible with your installation by checking
beancount --version.
Double-Entry Principles
Beancount enforces Double-Entry Bookkeeping. This system, codified by Luca Pacioli in 1494, is the standard for businesses worldwide because it is the only way to ensure mathematical consistency in financial records.
The fundamental rule: Sum = 0
In single-entry accounting (like a simple checkbook register), you just write down “Spent $50”. But where did that $50 come from? Did you pay cash? Credit card? Gift card?
In double-entry, you must describe the movement of value.
- Value came FROM somewhere.
- Value went TO somewhere.
Beancount models this mathematically: The sum of all postings in a transaction must be zero.
If you move $10 from your Left Pocket (-10) to your Right Pocket (+10), the total change in your wealth is zero (-10 + 10 = 0).
The Five Buckets
Everything in accounting fits into one of five buckets. In Beancount, we use positive and negative numbers to represent these, which simplifies the math.
| Category | Normal Sign | Interpretation |
|---|---|---|
| Assets | Positive (+) | Money you have (Cash, Bank, House) |
| Liabilities | Negative (-) | Money you owe (Credit Cards, Loans) |
| Income | Negative (-) | Money you earned (Salary, Interest) |
| Expenses | Positive (+) | Money you spent (Food, Rent) |
| Equity | Negative (-) | Your Net Worth (Assets - Liabilities) |
Why is Income negative?
This is often the most confusing part for beginners. Think of it this way:
Income is money leaving the “External World” and entering “Your World”.
When your employer pays you $1000:
- They lose $1000. Beancount tracks this as
Income:Salarywith a value of-1000. - You gain $1000. Your
Assets:Checkinggoes up by+1000.
-1000 (Income) + 1000 (Asset) = 0. The transaction balances.
Why are Expenses positive?
When you buy coffee for $5:
- You lose $5 cash.
Assets:Cashis-5. - You gain $5 worth of “Coffee Value”.
Expenses:Foodis+5.
-5 (Asset) + 5 (Expense) = 0.
The Equity Equation
Because all transactions sum to zero, the sum of ALL accounts in your file must also equal zero at all times.
Assets + Liabilities + Income + Expenses + Equity = 0
If we rearrange this, we get the definition of Net Worth:
(Assets + Liabilities) + (Income + Expenses) + Equity = 0
(Assets + Liabilities)is your current wealth.(Income + Expenses)is your profit for the year.Equityis your historical wealth.
Continuous Accounting
Traditional accounting software (and corporate accounting) relies on Fiscal Years. At the end of the year, accountants “close the books.”
This involves:
- Summing up all Income and Expenses.
- Moving that net profit/loss to
Equity:Retained-Earnings. - Resetting Income and Expenses to zero for Jan 1st.
Why Beancount is Different
Beancount uses Continuous Accounting. It never resets accounts to zero.
Advantages
- Multi-Year Reporting: You can run a report for “The last 18 months” or “2010 to 2020” instantly. In a closed system, this requires stitching together multiple closed files.
- Simplicity: You don’t have to create complex “closing entries” every year.
- Data Integrity: Your file is a single, unbroken stream of history.
How it works
When you want to see your income for just 2024, you don’t look at the raw balance (which is “All Income since the beginning of time”). Instead, you ask Beancount to calculate the change in balance between Jan 1, 2024, and Dec 31, 2024.
CLI tools do this automatically when you pass --year=2024.
Trade vs. Settlement Dates
In financial markets, a transaction happens in two distinct stages:
- Trade Date (T): The legal agreement to buy/sell is executed. The price is fixed.
- Settlement Date (T+2): The legal ownership is transferred and cash is exchanged.
The Accounting Dilemma
This delay creates a gap in your records.
- On Date T: You “own” the stock risk (if price moves, you gain/lose), but you still have the cash in your account.
- On Date T+2: The cash actually leaves.
Which date should you use for your books?
Model 1: Trade Date Accounting (Standard)
Beancount (and the IRS) generally favors Trade Date Accounting.
- Concept: You record the asset change immediately on Day T.
- Reality: Your Cash account in Beancount will show a lower balance than your actual bank balance for 2 days.
- Why: Tax liability usually attaches on the Trade Date. The holding period (Long vs Short term capital gains) starts counting from the Trade Date.
Model 2: Settlement Date Accounting (Cash Basis)
Some users prefer to wait until the cash moves.
- Concept: You ignore the trade until T+2.
- Problem: Your Portfolio Value is wrong for 2 days. You might miss tax deadlines if the trade happens on Dec 31st but settles Jan 2nd.
Model 3: Full Accrual (The “Pending” Asset)
To be perfectly accurate during the gap, you must recognize that you owe money (a Payable) or are owed money (a Receivable).
On Trade Date: You don’t lose Cash yet; you gain a specific liability (“Settlement Payable”).
2024-06-01 * "Buy Stock"
Assets:Brokerage:Stock 10 AAPL {100.00 USD}
Liabilities:Brokerage:Settlement -1000.00 USD
On Settlement Date: The liability is extinguished by Cash.
2024-06-03 * "Settlement"
Liabilities:Brokerage:Settlement 1000.00 USD
Assets:Brokerage:Cash -1000.00 USD
While Model 3 is the most “correct” for an auditor, Model 1 (Trade Date) is the standard recommendation for personal finance in Beancount because it simplifies data entry significantly.
How Inventories Work
In many accounting systems, currencies are simple numbers. In Beancount, any account can hold “Inventory.”
What is Inventory?
Think of an inventory as a mixed bag of items. A single account doesn’t just hold “Dollars.” It can hold:
- 100 USD
- 50 EUR
- 10 shares of GOOG (bought at $100)
- 5 shares of GOOG (bought at $120)
Beancount tracks each of these “lots” separately.
Cost Basis: The Specific Lot
When you acquire a commodity (like stock), Beancount remembers the cost.
2024-01-01 * "Buy"
Assets:Brokerage 10 GOOG {100.00 USD}
This creates a specific lot in your inventory: 10 GOOG @ 100 USD.
If you buy more later:
2024-02-01 * "Buy More"
Assets:Brokerage 5 GOOG {120.00 USD}
Your Assets:Brokerage account now holds two distinct lots. It does not hold “15 GOOG at average cost $106”. It holds the specific lots.
Reduction (Selling)
When you sell, you must specify which lot you are reducing. Beancount needs to know this to calculate the Capital Gain correctly.
How-to: For step-by-step instructions, see Recording Trades.
2024-03-01 * "Sell"
Assets:Brokerage -5 GOOG {100.00 USD} @ 130.00 USD
Here, we explicitly told Beancount “Sell 5 shares from the lot that cost $100”.
- Cost: 5 * 100 = 500
- Price: 5 * 130 = 650
- Profit: 150
If you tried to sell {-5 GOOG} without specifying the cost {...}, Beancount wouldn’t know which lot to pick and would raise an error (unless you use a booking method like FIFO, which Beancount 3 can automate in some contexts).
Strict vs Average Booking
Beancount uses a Strict Booking model by default, often called “Specific Identification” in tax terms.
Why Strict?
In many jurisdictions (like the US), the tax authority allows you to choose which shares you are selling.
- Optimization: If you sell shares with a high cost basis, you realize a smaller capital gain (and pay less tax now).
- Control: If you want to hold onto your oldest shares (for long-term capital gains rates), you need to explicitly sell the newer ones.
If Beancount automatically averaged your costs (e.g., merging the $100 and $120 lots into one $106.66 lot), you would lose this ability to optimize and report correctly.
What about Average Cost?
Some countries (like Canada or the UK) require you to use the Average Cost Basis (ACB) method. Beancount 3 handles this by allowing you to merge lots.
- Manual: You can use a transaction to “convert” multiple specific lots into a single average lot, though this is tedious.
- Automatic: Beancount 3 introduces flexible booking methods that can simulate average cost behavior during reduction, or you can use plugins to enforce it.
However, even with Average Cost, the underlying database in Beancount prefers strictness to ensure auditability. You can always derive an average from specific lots, but you cannot recover specific lots if you strictly averaged them on input.
Rounding and Precision
Computers often struggle with decimal numbers (e.g., 0.1 + 0.2 might equal 0.30000000000000004). Beancount avoids this by using Decimal types, not floating-point numbers.
Precision Inference
Beancount does not have a hardcoded list of currencies. Instead, it infers the precision from your file.
If you write:
2024-01-01 * "Test"
Assets:Cash 10.500 USD
Beancount will assume USD has 3 decimal places for the entire file.
Best Practice: Always be consistent. Use 10.00 USD, not 10 USD.
The Tolerance Problem
In real life, splitting bills or currency conversion often results in fractions of a cent.
Example: Splitting $10.00 three ways.
- Person A: 3.33
- Person B: 3.33
- Person C: 3.33
- Total: 9.99
The transaction is off by 0.01.
Automatic Tolerance
Beancount allows a small tolerance (default 0.005) for transactions to balance. If the difference is within this tolerance, Beancount accepts it.
Accumulating Rounding Errors
If you want to track these discrepancies explicitly (or if they are larger than the tolerance), use the account_rounding option.
option "account_rounding" "Equity:Rounding"
When this is enabled, Beancount will automatically add a posting to Equity:Rounding to make the transaction balance perfectly.
2024-01-01 * "Split Bill"
Expenses:Food 10.00 USD
Assets:A -3.33 USD
Assets:B -3.33 USD
Assets:C -3.33 USD
; Implicitly adds:
; Equity:Rounding -0.01 USD
Why Beancount 3?
Beancount 3 represents a significant evolution of the Beancount project. While the syntax remains largely backward-compatible with v2, the underlying engine and ecosystem have been completely overhauled.
This guide outlines the key improvements and differences for users migrating from v2 or those curious about the architecture.
1. Core Architecture & Performance (The C++ Rewrite)
The most significant change in v3 is the move from a pure Python implementation to a C++ core.
- v2: Was primarily Python, with some C extensions for speed. As ledgers grew (with 10+ years of history), loading times could become noticeable (taking seconds to minutes).
- v3: The core logic (parsing, booking, interpolation) is written in C++.
This results in:
- Drastic Performance Gains: Loading and checking large files is orders of magnitude faster.
- Scalability: Beancount 3 can handle massive transaction volumes (decades of data) without UI lag or slow reporting.
2. Strictness, Correctness, and Type Safety
Beancount 3 is stricter about accounting invariants. The rewrite allowed the authors to tighten the rules and ensure calculations are even more precise—critical for financial software.
- Lot Matching: The logic for matching lots (e.g., selling stocks) is more robust.
- Sign Handling: Ambiguities in sign handling (mixed positive/negative numbers in confusing contexts) are reduced or disallowed.
- Precision: Floating-point issues are further minimized by the C++ decimal implementation.
3. Focus on the Core & Modular Ecosystem
Beancount 3 focuses on doing the core accounting tasks perfectly. It adopts a more modular philosophy, separating the core engine from auxiliary tools.
Importing Data (Beangulp)
In Beancount v2, the importing framework (beancount.ingest) was bundled directly into the main package.
In v3, ingestion is split into a separate project called Beangulp.
- This separates the concerns of ledger logic from data extraction logic.
- Beangulp provides a cleaner API and better tools for deduplicating transactions.
- Existing importers will likely need minor updates to work with Beangulp.
Modern Tooling
Beancount 3 is designed to work better with modern Python environments.
- Fava: The popular web UI continues to support Beancount 3.
- Bean-query: The SQL engine has been refined, though the syntax (BQL) remains mostly the same.
- API Stability: For developers building tools on top of Beancount, v3 provides a more stable, typed API (via Python bindings to the C++ core).
4. Installation and Distribution
Because of the C++ core, installation is slightly different than the pure Python days of v2.
- v2: Simple
pip install beancount. - v3: Requires compiling the C++ core or using pre-built binary wheels.
- The build system uses Bazel, ensuring reproducible builds.
- Most users should stick to official binary releases to avoid complex compilation steps.
Summary for Migrating Users
- Your
.beancountfiles likely don’t need changes. The syntax is stable. - Your importers will need attention. You will likely need to migrate to Beangulp.
- Your scripts might break. Internal Python API changes will affect custom tools.
- It will be much faster.