What You Are Building: A Personal Budget Tracker
A personal budget tracker script helps you record money coming in (income) and money going out (expenses), then summarizes where your money is going. In this chapter, you will build a small command-line program that can: add transactions, categorize them, compute totals, show a monthly summary, and warn you when spending exceeds a limit you set for a category.
Instead of focusing on basic Python syntax, this chapter focuses on how to design the script: what data you store, how you represent transactions, how you calculate summaries, and how you structure features so the tool stays easy to extend.
Designing the Data Model (Transactions and Categories)
Before writing code, decide what a “transaction” means in your tracker. A practical transaction record usually contains:
- date (for monthly summaries and sorting)
- description (what the transaction was)
- category (Food, Rent, Transport, etc.)
- amount (positive number for income, negative for expense, or store a type field)
- type (income/expense), if you prefer amounts always positive
For beginner-friendly budgeting, it is often clearer to store amount as a positive number and also store a kind field that is either "income" or "expense". This avoids confusion when you later sum totals by type.
Also decide how you will store categories and optional category budgets (limits). A simple approach is a dictionary mapping category name to a monthly limit:
Continue in our app.
You can listen to the audiobook with the screen off, receive a free certificate for this course, and also have access to 5,000 other free online courses.
Or continue reading below...Download the app
category_limits = {"Food": 300.0, "Transport": 120.0, "Rent": 900.0}Transactions can be stored as a list of dictionaries (or later, as a list of small objects). A dictionary-based record stays readable:
transaction = {"date": "2026-01-12", "description": "Groceries", "category": "Food", "kind": "expense", "amount": 42.50}Even if you already know other ways to model data, this structure is easy to print, filter, and summarize.
Choosing Features for a First Useful Version
It’s tempting to add everything (charts, bank imports, recurring bills). For a first version, focus on features that make the script genuinely useful while staying small:
- Add an income transaction
- Add an expense transaction
- List transactions for a month
- Show summary totals (income, expenses, net)
- Show spending by category
- Set or update category limits
- Warn if a category is over its limit
Notice that these features are all based on the same core operations: store transactions, filter by month, and aggregate totals.
Step-by-Step Plan (From Empty File to Working Tracker)
Step 1: Decide the input format for dates and months
Monthly summaries require a consistent date format. A common choice is YYYY-MM-DD. For selecting a month, you can ask for YYYY-MM (for example, 2026-01). Then you can check whether a transaction belongs to that month by comparing the first 7 characters of the date string.
This approach avoids complex date parsing for a beginner project, while still being reliable as long as users follow the format.
Step 2: Create helper functions for calculations
Your script will repeatedly compute totals. Create small calculation helpers that accept a list of transactions and return numbers or dictionaries. This keeps the “menu” code clean and makes it easier to test pieces separately.
Here are the core calculations you typically need:
- Total income for a set of transactions
- Total expenses for a set of transactions
- Net (income minus expenses)
- Expenses grouped by category
def total_by_kind(transactions, kind):
return sum(t["amount"] for t in transactions if t["kind"] == kind)
def expenses_by_category(transactions):
totals = {}
for t in transactions:
if t["kind"] != "expense":
continue
cat = t["category"]
totals[cat] = totals.get(cat, 0.0) + t["amount"]
return totalsThese helpers assume each transaction has the same keys. That consistency is why the data model matters.
Step 3: Filter transactions by month
Filtering is the bridge between “all-time data” and “monthly budget.” Create a function that returns only the transactions in a given month string:
def filter_by_month(transactions, month_yyyy_mm):
return [t for t in transactions if t["date"][:7] == month_yyyy_mm]Now every summary can be computed on the filtered list.
Step 4: Build the menu actions (add, list, summarize)
In a command-line tracker, a simple menu loop is enough. Each menu option calls a function that performs one task. The key is to keep each task focused: gather inputs, create a transaction record, append it to the list, then return to the menu.
When adding a transaction, you need to collect:
- Date
- Description
- Category
- Kind (income/expense)
- Amount
Also consider category consistency. If you allow free typing, users might enter Food, food, and FOOD as separate categories. A simple rule is to normalize categories (for example, title case):
def normalize_category(text):
return text.strip().title()Then every time you store a category, you store the normalized version.
Full Script Example (Single-File Budget Tracker)
The following script shows a complete first version. It stores transactions in memory while the program runs and includes category limits. You can later connect it to file storage or other persistence, but the focus here is the budgeting logic and structure.
def normalize_category(text):
return text.strip().title()
def filter_by_month(transactions, month_yyyy_mm):
return [t for t in transactions if t["date"][:7] == month_yyyy_mm]
def total_by_kind(transactions, kind):
return sum(t["amount"] for t in transactions if t["kind"] == kind)
def expenses_by_category(transactions):
totals = {}
for t in transactions:
if t["kind"] != "expense":
continue
cat = t["category"]
totals[cat] = totals.get(cat, 0.0) + t["amount"]
return totals
def print_month_summary(transactions, category_limits, month_yyyy_mm):
month_tx = filter_by_month(transactions, month_yyyy_mm)
income = total_by_kind(month_tx, "income")
expenses = total_by_kind(month_tx, "expense")
net = income - expenses
print("\n=== Summary for", month_yyyy_mm, "===")
print(f"Income : {income:.2f}")
print(f"Expenses: {expenses:.2f}")
print(f"Net : {net:.2f}")
by_cat = expenses_by_category(month_tx)
if not by_cat:
print("\nNo expenses recorded for this month.")
return
print("\nExpenses by category:")
for cat in sorted(by_cat):
spent = by_cat[cat]
limit = category_limits.get(cat)
if limit is None:
print(f"- {cat}: {spent:.2f} (no limit set)")
else:
status = "OK" if spent <= limit else "OVER"
print(f"- {cat}: {spent:.2f} / {limit:.2f} [{status}]")
def list_transactions(transactions, month_yyyy_mm=None):
tx = transactions
if month_yyyy_mm:
tx = filter_by_month(transactions, month_yyyy_mm)
if not tx:
print("\nNo transactions to show.")
return
print("\nDate Kind Category Amount Description")
print("-" * 60)
for t in tx:
date = t["date"]
kind = t["kind"].ljust(7)
cat = t["category"].ljust(14)
amt = f"{t['amount']:.2f}".rjust(8)
desc = t["description"]
print(f"{date} {kind} {cat} {amt} {desc}")
def add_transaction(transactions, kind):
print("\nAdd", kind)
date = input("Date (YYYY-MM-DD): ").strip()
description = input("Description: ").strip()
category = normalize_category(input("Category: "))
while True:
amt_text = input("Amount (positive number): ").strip()
try:
amount = float(amt_text)
if amount <= 0:
print("Please enter a positive number.")
continue
break
except ValueError:
print("Please enter a valid number.")
transactions.append({
"date": date,
"description": description,
"category": category,
"kind": kind,
"amount": amount
})
print("Saved.")
def set_category_limit(category_limits):
print("\nSet category limit")
category = normalize_category(input("Category: "))
while True:
limit_text = input("Monthly limit amount: ").strip()
try:
limit = float(limit_text)
if limit < 0:
print("Limit cannot be negative.")
continue
break
except ValueError:
print("Please enter a valid number.")
category_limits[category] = limit
print(f"Limit set: {category} => {limit:.2f}")
def main():
transactions = []
category_limits = {}
while True:
print("\n=== Personal Budget Tracker ===")
print("1) Add income")
print("2) Add expense")
print("3) List all transactions")
print("4) List transactions for a month")
print("5) Show monthly summary")
print("6) Set category limit")
print("7) Show category limits")
print("0) Exit")
choice = input("Choose: ").strip()
if choice == "1":
add_transaction(transactions, "income")
elif choice == "2":
add_transaction(transactions, "expense")
elif choice == "3":
list_transactions(transactions)
elif choice == "4":
month = input("Month (YYYY-MM): ").strip()
list_transactions(transactions, month)
elif choice == "5":
month = input("Month (YYYY-MM): ").strip()
print_month_summary(transactions, category_limits, month)
elif choice == "6":
set_category_limit(category_limits)
elif choice == "7":
if not category_limits:
print("\nNo limits set.")
else:
print("\nCategory limits:")
for cat in sorted(category_limits):
print(f"- {cat}: {category_limits[cat]:.2f}")
elif choice == "0":
break
else:
print("Please choose a valid option.")
if __name__ == "__main__":
main()This version is already useful: you can record transactions, view a month, and see whether you are over budget in any category.
Understanding the Budget Calculations
Income, expenses, and net
The script calculates totals by scanning the transactions and summing amounts based on kind. This is a common pattern in budgeting: totals are derived from the raw transaction list rather than stored separately. That way, if you later edit or remove a transaction, the totals automatically reflect the change.
Net is computed as:
net = income - expensesBecause expenses are stored as positive numbers, you subtract them. If you had stored expenses as negative numbers, net would be a simple sum of all amounts, but the category summaries and printing would require more care. Either approach works; the important part is to be consistent.
Spending by category
To understand where money goes, you group expenses by category. The script builds a dictionary where each key is a category and each value is the total spent in that category for the selected month.
This is the core of many budgeting tools: once you can group by category, you can add features like “top 3 categories,” “percentage of spending,” or “compare to last month.”
Limits and warnings
Category limits are optional. If a category has a limit, the summary prints OK or OVER. This is a simple but powerful feedback mechanism: it turns the tracker from a passive log into an active budgeting assistant.
Notice that limits are checked only for expenses, not income. If you want, you can also add an “income goal” category, but most budgets focus on controlling spending.
Practical Improvements You Can Add Next (Without Changing the Core)
Add a “quick add” for common categories
If you often use the same categories, you can show a numbered list and let the user choose instead of typing. The key idea is to reduce friction so you actually keep using the tracker.
For example, keep a list of known categories and display them before asking for input. If the user types a number, map it to the category; otherwise treat the input as a new category name.
Support splitting a transaction across categories
Sometimes one purchase belongs to multiple categories (for example, a supermarket trip that includes groceries and household items). A simple way to support this is to enter multiple transactions with the same date and description but different categories and amounts. Your summary logic already supports this because it only cares about the list of transactions.
Add a “monthly budget overview” that shows remaining amounts
Instead of only showing spent / limit, you can compute remaining budget:
remaining = limit - spentThen print something like “remaining 45.20” or “over by 12.30.” This makes the summary more actionable.
Sort and format transactions for readability
When listing transactions, you can sort by date and then by category. If you keep dates in YYYY-MM-DD format, string sorting works correctly for chronological order.
tx_sorted = sorted(tx, key=lambda t: (t["date"], t["kind"], t["category"]))Then print tx_sorted instead of the original list.
Testing Your Tracker with Realistic Data
To verify your script behaves correctly, enter a small set of transactions for a month and check the summary math. For example:
- Income: 2026-01-01 Salary 2500
- Expense: 2026-01-02 Rent 900
- Expense: 2026-01-03 Food 60
- Expense: 2026-01-10 Transport 25
- Expense: 2026-01-15 Food 45
If you set limits like Food 300, Transport 120, Rent 900, your summary should show expenses by category and mark Rent as OK exactly at the limit. This kind of manual test is valuable because budgeting scripts must be trustworthy; small mistakes in totals can make the tool frustrating to use.
Common Budget-Tracker Pitfalls (and How to Avoid Them)
Inconsistent categories
If categories are inconsistent, your “Food” spending might be split across “food” and “Food.” Normalizing category input (for example, using .title()) prevents many of these issues. Another approach is to restrict categories to a predefined list, but that can feel limiting early on.
Mixing monthly and all-time numbers
Be careful to compute monthly summaries from the filtered list, not from all transactions. A good habit is to name variables clearly (for example, month_tx) and only pass monthly data into monthly calculations.
Forgetting that income and expenses are different concepts
If you store both as positive amounts, your totals must separate them by kind. If you store expenses as negative amounts, your category totals should use absolute values or you will print negative “spent” numbers. Pick one method and keep it consistent across the script.
Not validating numeric input enough
Budget tools deal with money, so you should reject negative amounts when adding an income or expense (unless you intentionally support refunds as negative expenses). In the example script, the amount must be a positive number, and the kind determines whether it is income or expense.