How I Conquered My Finances with Free Software

 

Photo by Andre Taissin on Unsplash

When making a big purchase (think an appliance or expensive tech gadget), I was often faced by questions like “How much can you afford to pay?”, “What is your budget?”,… My answer was usually a guesstimate based on my research into typical prices of the commodity I’m buying. They weren’t really about whether I could afford the thing, but about how much I thought was reasonable to pay for it.

Deciding whether I could afford something, it turns out, required me to have a clear picture of my finances and a budget. It took me a depressingly long time to figure this out. It also took a lot of effort to correctly capture my financial picture once I did. I am, however, in a much better place for it.

Through online reading and conversations with my friends I came across zero-based budgeting and the You Need a Budget (YNAB) system. In a nutshell, the idea is to assign every dollar of your income a job: paying rent, going out, saving for a car,… This way, you know exactly what you can afford to spend on each category and you can plan for emergencies much better.

OK. So I had a system in mind, but what then? How should I implement it? As a free software enthusiast and a hobbyist programmer, I thought that surely I didn’t need to resort to paid software for that. It turns out I really didn’t. Not with a very featureful free software like ledger-cli already available. This article is about how I use ledger with some Python scripts of my own to keep my finances under control and even improve my financial situation.

The Philosophy of Ledger

Press enter or click to view image in full size
Photo by Kelly Sikkema on Unsplash

Ledger’s philosophy is very simple: it’s a double-entry bookkeeping software. This means that every entry/transaction you record has to be associated with at least two accounts, the one it’s taking money from and the one it’s putting money into. This approach means that all your transactions must finally balance to zero. It also means you know exactly where every cent is coming from and going to.

Ledger also has the concept of commodities. A commodity in Ledger is any unit of value that you want to keep track off (dollars, yen, minutes of time, a particular stock, sacks of potato,…). You can also define exchange rates between different commodities and record your transaction in any mix of commodities you need then have Ledger convert between them as necessary.

Even though Ledger has excellent facilities for entering many pieces of information about your transaction, where it really shines is reporting. Ledger makes it really easy to glean all sorts of information and reports from your list of transactions and even plot graphs showing things like the increase in your net worth over time or the composition of your portfolio on a given date.

The best thing about Ledger, to my mind, is the fact that it keeps track of all this information in a plain text file (or a collection thereof). This means that your information is future-proof and very easy to keep track of using things like revision control, for example.

Basic Examples

So what does this look like in practice? A simple transaction consists of a date and several account entries:

2025/09/28 * Grocery Store
liabilities:credit_card $-150.00
expenses:monthly:groceries $80.0
expenses:monthly:discretionary $70

The above transaction records a date, a transaction description (the “payee” in Ledger speak), the grocery store in this case and three account postings. The credit card account is the one the money is coming from so $150 is taken off it and the money is divided into two expense categories. Because all the entries must balance, one of the account postings may have its amount omitted. The following entry is equivalent to the previous one:

2025/09/28 * Grocery Store
liabilities:credit_card $-150.00
expenses:monthly:groceries
expenses:monthly:discretionary $70

Note that you can create a hierarchy in account names using : to separate different levels of the hierarchy. And that’s really all there is to it. You collect your transactions in a big file (the ledger file) or a collection of them and point ledger to them when you want to run a report.

Don’t let the simplicity of the above format fool you, though: Ledger is chock full of features! The * in the above posting for example indicates a transaction that has “cleared” but you can use ! for a transaction that is pending (e.g. a check that’s waiting to clear). You can add additional metadata with all sorts of information (e.g. the check number). You can make a posting containing different commodities (e.g. purchase of some company’s shares using some Euros). The possibilities are quite vast. My advice is to start with a small feature set and expand to fit your needs.

One feature I use a lot is virtual accounts. These are accounts that don’t have to be balanced. My (and indeed many other people’s) main use case for them? Creating funds! These virtual accounts can be as many buckets as you want to allocate your money to. You can use them to easily keep track of what you can afford in each area of your life. Here’s a typical transaction illustrating what I mean:

2025/09/01 * Salary
income:salary $-1000
assets:bank:checking_account
(budget:fund:car_maintenance) $500
(budget:fund:emergency) $500

Note the following:

  1. The “real” accounts have to balance, so I can omit the amount on the second account.
  2. Virtual account names are surrounded by parentheses.
  3. You can’t omit the amount for any virtual account posting since they are not required to balance (even though they do in this case).

When querying my ledger transactions, I can ask for virtual account postings to be omitted so I can focus on “real” accounts only or I can have the virtual postings included exclusively to find out how much money I have in each bucket/fund.

Please don’t make the mistake I made. I learned about Ledger casually through some YouTube videos and went on to implement my system. The result was a mish-mash of Python scripts that tried to re-implement functionality that I didn’t realize Ledger already had. It took me a long time to clean that mess up. Do yourself a favor and read the manual first. It might feel overwhelming with all the features Ledger has, but it will give you a sense of what is possible for when you need it.

Querying the Ledger

As I mentioned above, the real utility of the Ledger system is the myriad reports and queries it allows you to create. Its query language is also very close to natural English, especially when you use meaningful account names. Here’s a query I often run to find out how much money I made from my side hustles:

ledger -f my_ledger_file.txt balance income and not salary

Here’s the response I get:

Ledger Balance Query Example(screenshot by author)

I’m asking Ledger to look at my transactions file and report a total balance of postings whose account matches income but does not match salary. Note that everything is reported in negative numbers because all my income accounts are accounts money comes from (to go into my bank account). I can pass the --invert option to invert the sign on all amounts if needed.

What if the balance reports some weird numbers and I want a detailed record of how that balance came about? I use the register command instead:

ledger -f my_ledger_file.txt register income and not salary

And here’s a snippet of what this produces:

Press enter or click to view image in full size
Ledger Register Query Example (screenshot by author)

Here the date and description of each transaction is reported, with the account name in the middle. The last two columns (sorry, they’re all blurred for my privacy) report the amount for each transaction and a running total. It’s very easy to pinpoint where everything is coming from.

Advanced Features

Ledger has a lot of advanced features for many different kinds of personal finance scenarios. I want to bring up 3 particular ones here because they have a lot of bearing on conquering one’s personal finances.

Periodic Transactions and Budgeting

You can set up a special type of transaction to recur periodically. This is for things like rent, estimated groceries expenses, etc… in short: to create a budget. This helps with the idea of “giving every dollar of your income a job”. It’s very easy to set up periodic transactions: just start the transaction description with ~ and give it a period expression. Here are some examples from my budget:

Periodic transactions to set up a budget (screenshot by author)
More periodic transactions. Note that you can restrict the period to which they apply (screenshot by author)

Also notice above that you can use [...] to indicate virtual transactions that need to balance. This way, I can omit the amount for money that has yet to be allocated to a bucket. It gets auto-calculated by balancing against the money I did allocate. I can later query the budget:unallocated account to find out how much money still doesn’t “have a job”.

The wonderful thing about budgeting this way is that Ledger can give me a comprehensive budget report:

ledger -f my_ledger.txt budget ^budget and not unallocated and not fund\
--begin september --end oct

In the above command, the first “budget” is the command to produce the budget report and ^budget is to match any accounts whose name starts with “budget”.

This gives me something like:

Press enter or click to view image in full size
Budget report (screenshot by author)

The first column shows me what I actually spent, the second column shows me what I had budgeted and the third column shows the deficit or surplus (with the percentage column showing the percentage of the budget that I used). This shows me that I spent a bit more than I had budgeted in September and I can immediately see the culprit categories. Very useful for next month!

Automated Transactions

Automated transactions allow you to write an expression that, when matched by any transaction, can add information to it. Here’s an example from the documentation:

= food
(Budget:$account) 10

2012-03-10 KFC
Expenses:Food $20.00
Assets:Cash

The automated transaction (the one that starts with =) says that for any transaction with a posting matching “food”, create a virtual posting to the account name “Budget:…” where … is the matching posting’s account name. The transaction amount should be 10 times the amount of the matching posting (note that 10 is bare without a commodity so it’s taken as a multiplier). With the example transaction, the above is equivalent to:

2012-03-10 KFC
Expenses:Food $20.00
(Budget:Expenses:Food) $200.00
Assets:Cash $-20.00

This can be a really handy way of managing your budget and buckets. I don’t use it much myself since I rely on Python for that (see below), but it might be all you need to get set up.

Stock Quotes

If you have any investments, part of being aware of your financial situation is to know how much your portfolio is worth at a given moment in time. By default, Ledger considers each stock symbol as a different commodity but you can pass the --market switch to have it convert these commodities to their market value. The way this is done is by keeping a price database. A typical price database entry looks like this:

P 2025/09/30 09:00:00 XYZ $50.00

This indicates that the price of the XYZ commodity at the given date and time was $50.00 per unit (e.g. per share). How does this price database get filled then? Simple, when you pass the --download switch to Ledger, it will search your system’s PATH environment variable for an executable named getquote and pass it the commodity whose price it needs as a command line argument. The script is then responsible for downloading the price and adding the appropriate entry to the price database. Below, I will show my Python implementation for such a script.

This facility has been very handy in tracking my net worth and it makes the Ledger system even more usable.

My System

Press enter or click to view image in full size
Photo by Marko Lengyel on Unsplash

So perhaps at this point you’re asking yourself the obvious question: where are all the transaction data coming from? I never mentioned anything about linking my bank/brokerage accounts with Ledger. And indeed Ledger does not support that, unlike most commercial apps. To me this is a feature not a shortcoming. For my own security, I don’t like giving third-party apps access to any of my financial accounts.

What I do is very much in line with Ledger’s philosophy: plain text files. I download data from my financial institutions in CSV file format and convert each transaction to Ledger entries. Ledger does offer its own facilities for converting a CSV file to Ledger file format, but I rely on my own Python script to add my own customization.

Here’s an overview of my system. Every week I:

  • Download all my financial data for the past week. The CSV file from each institution goes into its own directory. The directory name allows my script to determine which account each file belongs to.
  • Run a script that goes over the transaction data and does the following:
    – Checks a long list of automated rules I have. If a transaction matches one of the automated rules, it creates an entry for it automatically. This is useful for things that don’t change like rent and subscriptions.
    – If no automatic rules are matched, it prompts me to enter a category for the expense/income represented by the transaction.
  • After I run the script, I check my budget report for the week and if there are any funds in the unallocated budget category, I manually create a transaction to reallocate them to the appropriate (virtual) buckets.
  • At the end of the month, an automated script “refills” my budget categories. If any budget accounts end up with a surplus, it gets put back into the appropriate “fund” bucket. If a budget account ends up with a deficit, it gets replenished (to zero, since next month’s budget will put positive funds in it) from the correct “fund” bucket, thereby reducing the available money I have saved in that category.
  • The automated script also does a very important job: it compares my credit card balance at the beginning of the month and at the end. If the balance at the end of the month is lower, this means I have paid a net positive amount towards my credit card debt and this needs to be deducted from my funds. If the balance is higher, it means I have more money on hand/money spent so it needs to be added back into my budget. The script thus adjusts my budget with the appropriate amount to keep it correctly in-line with the money actually in my account.

How Python Helps

Press enter or click to view image in full size
Photo by Rubaitul Azad on Unsplash

As I mentioned above, I rely on several Python scripts to help me (semi-)automate the process of keeping track of all aspects of my finances. I will show some relevant snippets here for inspiration. I’m not publishing all my code because it’s inextricably intertwined with private details of my finances.

First, the getquote script that downloads stock price quotes; it uses Yahoo Finance for that:

import yfinance as yf
from datetime import datetime as dt
from os import environ
import sys

def format_price(symbol: str, time: str, db: str) -> str:
ticker = yf.Ticker(symbol)
data = ticker.history()
quote = data['Close'].iloc[-1]
return f'P {time} {symbol} ${quote:.2f}'

PRICE_DB = environ["HOME"] + "/.pricedb" # Default location used by Ledger
time_now = dt.now().strftime("%Y/%m/%d %H:%M:%S")

try:
symbol = sys.argv[1]
except IndexError:
exit(0)
with open(PRICE_DB, 'a') as DB:
DB.write(format_price(symbol, time_now, PRICE_DB))
DB.write('\n')

The format_price function fetches the latest close price for the given stock symbol and returns a line in the Ledger price database format. The script takes its first command line argument and passes that to the function and writes the resulting entry to the database. The script makes no provisions for invalid stock symbols and will crash if called with an invalid stock symbol. This is fine for my purposes since I don’t call it myself but rely on Ledger to call it with the stock symbols in my portfolio.

For functions that interact with my Ledger, I need the ability to run Ledger queries from within Python and interact with their output. Here’s the function that does that:

from typing import Optional
from subprocess import run

def run_ledger_query(query: str, journals: list[str] | str, csv: Optional[bool] = True) -> str:
if not isinstance(journals, list):
journals = [journals]
journals_cmd = reduce(add, [['-f', j] for j in journals])
cmd = ['ledger', *journals_cmd] + (['csv'] if csv else []) + [*re.split(r'\s+', query)]
proc = run(cmd, capture_output=True)
return proc.stdout.decode('utf-8')

By default, it relies on the csv command in Ledger which produces the output in CSV format for easy parsing in Python.

Here’s the function that reconciles my credit card balance at the end of the month:

import pandas as pd
from datetime import datetime as dt

def reconcile_credit_for_month(df: pd.DataFrame, month: int, year: int) -> str:
start = dt.strptime(f'{year}-{month:02d}-01', '%Y-%m-%d')
end = get_month_extreme(start) # Gets the last day of the month of the given date
credit_report = ledger_report(df, start, end, 'liabilities:.*:credit')
opening_balance = list(credit_report.opening_balance)[0]
closing_balance = list(credit_report.total_credit)[0] - list(credit_report.total_debit)[0]
if month != 1:
closing_balance += opening_balance
return f"""{end.strftime('%Y/%m/%d')} * Adjust budget for overpaid/unpaid credit
(budget:unallocated) ${opening_balance-closing_balance:.2f}
"""

The ledger_report function depends on a Pandas data frame that is the result of running a Ledger query and parsing the CSV output. It takes an account regex and start and end times and returns the postings that match these criteria. The end result is a transaction that gets added to my ledger file to adjust the balance of my budget.

As for my script that converts account statements from CSV format to Ledger format, it’s too big (and private) to share. I will say that it uses the directory structure to determine the account the file belongs to. It then dispatches on the account name to apply the correct function to convert CSV file to Ledger format.

I will show here two methods of my giant Ledger class that might be of interest. _get_virtual_account returns the virtual account associated with a given transaction based on a few rules I defined and _prompt_for_category prompts me to manually select one category from the available ones in my budget to classify a given transaction. Here’s the code:

 def _get_virtual_account(self, account: str) -> Optional[str]:
if account.startswith('income'):
return 'budget:unallocated'
elif account.startswith('expenses:fixed') or account.startswith('expenses:monthly'):
return re.sub('expenses', 'budget', account)
elif account.startswith('expenses'):
return re.sub(r'expenses', 'budget:fund', account)
elif account == 'assets:***:cash':
return 'budget:fund:investment'
else:
return None

The rules are pretty simple. Any income transaction gets added to the unallocated budget, which I will then manually allocate to different buckets via an additional transaction. If an expense transaction is part of my “fixed” expenses (rent, utilities, subscriptions, etc…) or my monthly budget, I subtract the amount from the similarly-named “budget” account. Any other expense transaction gets subtracted from the appropriate “fund” bucket. The final rule is ad hoc for a specific account that indicates some brokerage fees being charged so it balances against my investment bucket.

Here’s the code that prompts for transaction category:

def _prompt_for_category(self, xact: pd.Series, account: str, categories: dict[int, str]) -> Transaction:
selection = -1
while(not (1 <= selection <= len(categories))):
print(f"Select a category for {xact.Date}-{xact.Description[:30]} (${xact.Amount})\n")
for (num, category) in categories.items():
print(f'{num} - {category}')
selection = int(input('>> '))
accts = {account: f'${xact.Amount}',
categories[selection]: ''}
virtual_account = self._get_virtual_account(categories[selection])
if virtual_account:
vamount = xact.Amount
accts.update({f'({virtual_account})': f'${vamount}'})
return Transaction(xact.Date,
xact.Description,
accts)

I go through the transactions and display the (numbered) categories and prompt myself for a number indicating the number of the category corresponding to the transaction. I also get the virtual account name for this category and add it to the transaction as well. The Transaction class is a custom class I created for handling and formatting transactions.

Note that some transactions can require that their amounts be divided among several categories (think of shopping for both groceries and clothing from the same place, for example). For such transactions, I manually edit the ledger file later after the initial categorization. The script knows not to re-prompt me for an older transaction (thereby overwriting my manual additions) as it keeps track of the dates and descriptions of all the transactions previously logged.

Finally, I have a script that is run via cron job and it parses my expenses very week and produces a report of how they stack up against the monthly budget and how much I can afford to spend in each category in the following week. This helps me keep my spending under control and reminds me of how much I can afford.

Success Stories

Press enter or click to view image in full size
Photo by Alexander Grey on Unsplash

The system I have described above took me a lot of time and effort to get right, so it better have paid back in benefits. And it really has! The fantastic query features of Ledger mean that I can ask very detailed questions about my finances and quickly get an insight with numbers. Here are some concrete ways this setup has helped me so far:

  1. It was trivial to set up a taskbar widget that tracks my net worth and updates it with the latest stock quotes. At any given moment, I know exactly how much I have in cash, stocks, and other financial instruments. This is currently helping me plan for big purchases down the road.
  2. I have used the reporting abilities to find out how much I spend on average each month on things like utilities, subscriptions and eating out.
  3. I have pet insurance for my dog and I kept wondering whether it was worth paying into it. Recently I realized that it’s very easy to determine that. I just asked Ledger how much I had paid into it so far this year and how much they have paid me back. Because my dog has an allergy that requires a daily pill, it turns out that the insurance policy paid me back a lot more than I paid into it. I went and checked my statements for the whole of last year and confirmed that this was the case in 2024 as well. I now know that keeping the insurance policy is definitely the right financial decision.
  4. I have used Ledger to track the growth of my alternative income streams over the past couple of years and the numbers show steady growth, not stagnation, which tells me I’m doing the right things.

Concluding Thoughts

To sum up, getting the right picture of my personal finances and staying on top of monthly spending has vastly improved my financial health and is helping me plan for future financial goals.

When I have an unexpected expense (need to fix something broken in my car, for example), I no longer stress over it, because I look at the appropriate fund bucket and see that there’s enough funds in there and that this expenditure, albeit unexpected, is still something that I had budgeted for in advance. Even though I’m not spending any less, the feeling of being in control of my budget softens the psychological blow and reduces my stress over the expense.

Using free software to track my finances was not an easy journey but it was the right choice for me as it let me completely own my own data in a future-proof format. I highly encourage you to take the plunge as well and learn Ledger (or a similar tool) for the sake of your own financial health.

Comments

Popular posts from this blog

6 Financial Blogs All Bloggers MUST Read

Fell in a hole, got out.

How can I make a million dollars on Medium?