import json
import os
from datetime import datetime
from pathlib import Path
from collections import defaultdict
try:
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
MATPLOTLIB_OK = True
except ImportError:
MATPLOTLIB_OK = False
# ============================================================
# CONFIGURATION
# ============================================================
DATA_FILE = "budget_data.json"
CURRENCY = "INR"
CURRENCY_SYM = "₹"
# Default categories
EXPENSE_CATEGORIES = [
"Food", "Transport", "Rent", "Utilities",
"Entertainment", "Healthcare", "Shopping",
"Education", "EMI / Loan", "Other"
]
INCOME_CATEGORIES = [
"Salary", "Freelance", "Business",
"Investment", "Gift", "Other"
]
# ============================================================
# LOAD & SAVE DATA
# ============================================================
def load_data():
if Path(DATA_FILE).exists():
try:
with open(DATA_FILE, "r") as f:
return json.load(f)
except:
pass
return {"transactions": [], "budgets": {}}
def save_data(data):
with open(DATA_FILE, "w") as f:
json.dump(data, f, indent=2)
# ============================================================
# FORMAT AMOUNT
# ============================================================
def fmt(amount):
return f"{CURRENCY_SYM}{amount:,.2f}"
# ============================================================
# ADD TRANSACTION
# ============================================================
def add_transaction(data, t_type):
"""Add an income or expense entry."""
print(f"\n Add {t_type.upper()}")
print(" " + "-"*40)
categories = INCOME_CATEGORIES if t_type == "income" else EXPENSE_CATEGORIES
print(" Categories:")
for i, cat in enumerate(categories, 1):
print(f" {i:>2}. {cat}")
try:
cat_idx = int(input("\n Choose category (number): ").strip()) - 1
if cat_idx < 0 or cat_idx >= len(categories):
print(" Invalid category.")
return
category = categories[cat_idx]
except ValueError:
print(" Invalid input.")
return
try:
amount = float(input(" Amount: ").strip())
if amount <= 0:
print(" Amount must be positive.")
return
except ValueError:
print(" Invalid amount.")
return
note = input(" Note (optional): ").strip()
date_input = input(" Date (DD-MM-YYYY) or Enter for today: ").strip()
if date_input:
try:
date = datetime.strptime(date_input, "%d-%m-%Y").strftime("%d-%m-%Y")
except ValueError:
print(" Invalid date format. Using today.")
date = datetime.now().strftime("%d-%m-%Y")
else:
date = datetime.now().strftime("%d-%m-%Y")
entry = {
"id": len(data["transactions"]) + 1,
"type": t_type,
"category": category,
"amount": amount,
"note": note,
"date": date,
"added_at": datetime.now().strftime("%d-%m-%Y %H:%M:%S")
}
data["transactions"].append(entry)
save_data(data)
icon = "+" if t_type == "income" else "-"
print(f"\n {icon} Saved: {category} — {fmt(amount)} on {date}")
if note:
print(f" Note: {note}")
# ============================================================
# VIEW TRANSACTIONS
# ============================================================
def view_transactions(data, month=None, t_type=None):
txns = data["transactions"]
# Filter by month (MM-YYYY)
if month:
txns = [t for t in txns if t["date"][3:] == month]
# Filter by type
if t_type:
txns = [t for t in txns if t["type"] == t_type]
if not txns:
print("\n No transactions found.")
return
# Sort by date descending
txns_sorted = sorted(txns,
key=lambda x: datetime.strptime(x["date"], "%d-%m-%Y"),
reverse=True)
print("\n" + "="*68)
label = f"{t_type.upper() if t_type else 'ALL'}"
label += f" [{month}]" if month else ""
print(f" TRANSACTIONS — {label} ({len(txns)} entries)")
print("="*68)
print(f" {'#':<5} {'DATE':<13} {'TYPE':<10} {'CATEGORY':<16} "
f"{'AMOUNT':>12} NOTE")
print(" " + "-"*63)
for t in txns_sorted[:50]: # show last 50
icon = "+" if t["type"] == "income" else "-"
note = t.get("note", "")[:20]
print(f" {t['id']:<5} {t['date']:<13} {t['type']:<10} "
f"{t['category']:<16} {icon}{fmt(t['amount']):>12} {note}")
if len(txns_sorted) > 50:
print(f" ... and {len(txns_sorted) - 50} more. Filter by month to see all.")
total_in = sum(t["amount"] for t in txns if t["type"] == "income")
total_out = sum(t["amount"] for t in txns if t["type"] == "expense")
balance = total_in - total_out
print(" " + "-"*63)
print(f" Total Income : {fmt(total_in)}")
print(f" Total Expense : {fmt(total_out)}")
b_sign = "+" if balance >= 0 else ""
print(f" Balance : {b_sign}{fmt(balance)}")
print("="*68)
# ============================================================
# MONTHLY SUMMARY
# ============================================================
def monthly_summary(data):
txns = data["transactions"]
if not txns:
print("\n No data yet.")
return
# Group by month
monthly = defaultdict(lambda: {"income": 0, "expense": 0,
"by_cat": defaultdict(float)})
for t in txns:
month = t["date"][3:] # MM-YYYY
monthly[month][t["type"]] += t["amount"]
if t["type"] == "expense":
monthly[month]["by_cat"][t["category"]] += t["amount"]
# Sort months chronologically
def month_key(m):
try:
return datetime.strptime(m, "%m-%Y")
except:
return datetime.min
sorted_months = sorted(monthly.keys(), key=month_key)
print("\n" + "="*60)
print(" MONTHLY SUMMARY")
print("="*60)
print(f" {'MONTH':<12} {'INCOME':>12} {'EXPENSE':>12} "
f"{'BALANCE':>12} STATUS")
print(" " + "-"*56)
for month in sorted_months:
m = monthly[month]
bal = m["income"] - m["expense"]
sign = "+" if bal >= 0 else ""
status = "SURPLUS" if bal >= 0 else "DEFICIT"
bar = _mini_bar(m["income"], m["expense"])
print(f" {month:<12} {fmt(m['income']):>12} "
f"{fmt(m['expense']):>12} "
f"{sign}{fmt(bal):>12} {status} {bar}")
# Overall totals
total_in = sum(m["income"] for m in monthly.values())
total_out = sum(m["expense"] for m in monthly.values())
total_bal = total_in - total_out
print(" " + "-"*56)
print(f" {'TOTAL':<12} {fmt(total_in):>12} "
f"{fmt(total_out):>12} "
f"{('+' if total_bal>=0 else '')}{fmt(total_bal):>12}")
print("="*60)
def _mini_bar(income, expense):
if max(income, expense) == 0:
return ""
scale = 12
i_bar = int((income / max(income, expense)) * scale)
e_bar = int((expense / max(income, expense)) * scale)
return f"I:{'█'*i_bar} E:{'█'*e_bar}"
# ============================================================
# CATEGORY BREAKDOWN
# ============================================================
def category_breakdown(data, month=None):
txns = data["transactions"]
if month:
txns = [t for t in txns if t["date"][3:] == month]
expenses = [t for t in txns if t["type"] == "expense"]
incomes = [t for t in txns if t["type"] == "income"]
if not expenses and not incomes:
print("\n No data for this period.")
return
def show_breakdown(items, label):
cat_totals = defaultdict(float)
for t in items:
cat_totals[t["category"]] += t["amount"]
total = sum(cat_totals.values())
if not cat_totals:
return
sorted_cats = sorted(cat_totals.items(), key=lambda x: x[1], reverse=True)
max_val = sorted_cats[0][1]
print(f"\n {label} (Total: {fmt(total)})")
print(" " + "-"*55)
print(f" {'CATEGORY':<18} {'AMOUNT':>12} {'%':>5} BAR")
print(" " + "-"*55)
for cat, amt in sorted_cats:
pct = (amt / total * 100) if total else 0
bar = "█" * int((amt / max_val) * 20)
print(f" {cat:<18} {fmt(amt):>12} {pct:>4.1f}% {bar}")
title = f"CATEGORY BREAKDOWN"
if month:
title += f" [{month}]"
print("\n" + "="*58)
print(f" {title}")
print("="*58)
show_breakdown(expenses, "EXPENSES")
show_breakdown(incomes, "INCOME SOURCES")
# ============================================================
# BUDGET GOALS
# ============================================================
def set_budget(data):
print("\n SET MONTHLY BUDGET LIMITS")
print(" Categories:")
for i, cat in enumerate(EXPENSE_CATEGORIES, 1):
current = data["budgets"].get(cat, 0)
current_str = f" (current: {fmt(current)})" if current else ""
print(f" {i:>2}. {cat}{current_str}")
try:
idx = int(input("\n Choose category: ").strip()) - 1
if idx < 0 or idx >= len(EXPENSE_CATEGORIES):
print(" Invalid.")
return
cat = EXPENSE_CATEGORIES[idx]
amt = float(input(f" Budget limit for {cat}: ").strip())
data["budgets"][cat] = amt
save_data(data)
print(f" Budget set: {cat} = {fmt(amt)}/month")
except ValueError:
print(" Invalid input.")
def check_budgets(data):
budgets = data.get("budgets", {})
if not budgets:
print("\n No budget limits set. Use option to set budgets.")
return
# Current month
this_month = datetime.now().strftime("%m-%Y")
txns = [t for t in data["transactions"]
if t["type"] == "expense" and t["date"][3:] == this_month]
cat_spent = defaultdict(float)
for t in txns:
cat_spent[t["category"]] += t["amount"]
print("\n" + "="*58)
print(f" BUDGET STATUS — {this_month}")
print("="*58)
print(f" {'CATEGORY':<18} {'BUDGET':>12} {'SPENT':>12} "
f"{'LEFT':>12} STATUS")
print(" " + "-"*54)
for cat, budget in sorted(budgets.items()):
spent = cat_spent.get(cat, 0)
left = budget - spent
pct = (spent / budget * 100) if budget else 0
bar = "█" * min(int(pct / 5), 20)
if pct >= 100:
status = "OVER BUDGET!"
elif pct >= 80:
status = "WARNING"
elif pct >= 50:
status = "ON TRACK"
else:
status = "OK"
print(f" {cat:<18} {fmt(budget):>12} {fmt(spent):>12} "
f"{fmt(left):>12} {status}")
print(f" {'':18} {pct:>5.1f}% used {bar}")
print("="*58)
# ============================================================
# MATPLOTLIB CHARTS (Optional)
# ============================================================
def plot_monthly_chart(data):
if not MATPLOTLIB_OK:
print("\n matplotlib not installed.")
print(" Install: pip install matplotlib")
return
txns = data["transactions"]
if not txns:
print("\n No data to plot.")
return
monthly = defaultdict(lambda: {"income": 0, "expense": 0})
for t in txns:
month = t["date"][3:]
monthly[month][t["type"]] += t["amount"]
def month_key(m):
try:
return datetime.strptime(m, "%m-%Y")
except:
return datetime.min
months = sorted(monthly.keys(), key=month_key)[-12:]
incomes = [monthly[m]["income"] for m in months]
expenses = [monthly[m]["expense"] for m in months]
balances = [i - e for i, e in zip(incomes, expenses)]
x = range(len(months))
width = 0.35
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 8))
fig.suptitle("Personal Budget Overview", fontsize=14, fontweight="bold")
# Bar chart — income vs expense
bars1 = ax1.bar([i - width/2 for i in x], incomes,
width, label="Income", color="#2ecc71", alpha=0.85)
bars2 = ax1.bar([i + width/2 for i in x], expenses,
width, label="Expense", color="#e74c3c", alpha=0.85)
ax1.set_title("Monthly Income vs Expense")
ax1.set_xticks(list(x))
ax1.set_xticklabels(months, rotation=45, ha="right")
ax1.set_ylabel(f"Amount ({CURRENCY})")
ax1.legend()
ax1.yaxis.set_major_formatter(
plt.FuncFormatter(lambda val, _: f"{CURRENCY_SYM}{val:,.0f}")
)
ax1.grid(axis="y", alpha=0.3)
# Line chart — balance
colors = ["#2ecc71" if b >= 0 else "#e74c3c" for b in balances]
ax2.bar(list(x), balances, color=colors, alpha=0.8)
ax2.axhline(0, color="black", linewidth=0.8, linestyle="--")
ax2.set_title("Monthly Balance (Surplus / Deficit)")
ax2.set_xticks(list(x))
ax2.set_xticklabels(months, rotation=45, ha="right")
ax2.set_ylabel(f"Balance ({CURRENCY})")
ax2.yaxis.set_major_formatter(
plt.FuncFormatter(lambda val, _: f"{CURRENCY_SYM}{val:,.0f}")
)
ax2.grid(axis="y", alpha=0.3)
plt.tight_layout()
plt.savefig("budget_chart.png", dpi=120, bbox_inches="tight")
print("\n Chart saved: budget_chart.png")
plt.show()
def plot_expense_pie(data, month=None):
if not MATPLOTLIB_OK:
print("\n matplotlib not installed.")
print(" Install: pip install matplotlib")
return
txns = data["transactions"]
if month:
txns = [t for t in txns if t["date"][3:] == month]
expenses = [t for t in txns if t["type"] == "expense"]
if not expenses:
print("\n No expense data to plot.")
return
cat_totals = defaultdict(float)
for t in expenses:
cat_totals[t["category"]] += t["amount"]
labels = list(cat_totals.keys())
values = list(cat_totals.values())
colors = plt.cm.Set3.colors[:len(labels)]
fig, ax = plt.subplots(figsize=(9, 7))
wedges, texts, autotexts = ax.pie(
values, labels=labels, autopct="%1.1f%%",
colors=colors, startangle=140,
wedgeprops={"edgecolor": "white", "linewidth": 1.5}
)
for at in autotexts:
at.set_fontsize(9)
title = f"Expense Breakdown"
if month:
title += f" — {month}"
ax.set_title(title, fontsize=13, fontweight="bold")
total = sum(values)
ax.text(0, -1.3, f"Total: {fmt(total)}",
ha="center", fontsize=11, color="#333")
plt.tight_layout()
plt.savefig("expense_pie.png", dpi=120, bbox_inches="tight")
print("\n Pie chart saved: expense_pie.png")
plt.show()
# ============================================================
# DELETE TRANSACTION
# ============================================================
def delete_transaction(data):
try:
tid = int(input("\n Enter transaction ID to delete: ").strip())
except ValueError:
print(" Invalid ID.")
return
for i, t in enumerate(data["transactions"]):
if t["id"] == tid:
confirm = input(f" Delete: {t['type']} {fmt(t['amount'])} "
f"— {t['category']} on {t['date']}? (y/n): ").strip().lower()
if confirm == "y":
data["transactions"].pop(i)
save_data(data)
print(" Deleted.")
else:
print(" Cancelled.")
return
print(f" Transaction ID {tid} not found.")
# ============================================================
# QUICK STATS
# ============================================================
def quick_stats(data):
txns = data["transactions"]
if not txns:
print("\n No data yet.")
return
this_month = datetime.now().strftime("%m-%Y")
this_txns = [t for t in txns if t["date"][3:] == this_month]
total_in = sum(t["amount"] for t in txns if t["type"] == "income")
total_out = sum(t["amount"] for t in txns if t["type"] == "expense")
balance = total_in - total_out
month_in = sum(t["amount"] for t in this_txns if t["type"] == "income")
month_out = sum(t["amount"] for t in this_txns if t["type"] == "expense")
month_bal = month_in - month_out
print("\n" + "="*50)
print(" QUICK STATS")
print("="*50)
print(f" This Month ({this_month}):")
print(f" Income : {fmt(month_in)}")
print(f" Expense : {fmt(month_out)}")
b = "+" if month_bal >= 0 else ""
print(f" Balance : {b}{fmt(month_bal)}")
print(f"\n All Time:")
print(f" Income : {fmt(total_in)}")
print(f" Expense : {fmt(total_out)}")
b = "+" if balance >= 0 else ""
print(f" Balance : {b}{fmt(balance)}")
print(f" Entries : {len(txns)}")
print("="*50)
# ============================================================
# MAIN MENU
# ============================================================
def print_menu():
print("\n" + "-"*48)
print(" PERSONAL BUDGET TRACKER")
print("-"*48)
print(" 1. Add income")
print(" 2. Add expense")
print(" 3. View transactions")
print(" 4. Monthly summary")
print(" 5. Category breakdown")
print(" 6. Budget limits & status")
print(" 7. Set budget limit for a category")
print(" 8. Plot monthly income vs expense chart")
print(" 9. Plot expense pie chart")
print(" 10. Quick stats")
print(" 11. Delete a transaction")
print(" 0. Exit")
print("-"*48)
def main():
print("\n" + "="*55)
print(" PERSONAL BUDGET TRACKER")
print("="*55)
data = load_data()
count = len(data["transactions"])
if count:
print(f"\n Loaded {count} transaction(s) from {DATA_FILE}")
else:
print(f"\n No data yet. Start by adding income or expenses.")
while True:
print_menu()
choice = input(" > ").strip()
if choice == "1":
add_transaction(data, "income")
elif choice == "2":
add_transaction(data, "expense")
elif choice == "3":
print("\n Filter options:")
print(" 1. All transactions")
print(" 2. By month")
print(" 3. Income only")
print(" 4. Expenses only")
sub = input(" Choice: ").strip()
if sub == "1":
view_transactions(data)
elif sub == "2":
month = input(" Month (MM-YYYY e.g. 04-2026): ").strip()
view_transactions(data, month=month)
elif sub == "3":
view_transactions(data, t_type="income")
elif sub == "4":
view_transactions(data, t_type="expense")
elif choice == "4":
monthly_summary(data)
elif choice == "5":
month = input("\n Month (MM-YYYY) or Enter for all time: ").strip()
category_breakdown(data, month=month or None)
elif choice == "6":
check_budgets(data)
elif choice == "7":
set_budget(data)
elif choice == "8":
plot_monthly_chart(data)
elif choice == "9":
month = input("\n Month (MM-YYYY) or Enter for all time: ").strip()
plot_expense_pie(data, month=month or None)
elif choice == "10":
quick_stats(data)
elif choice == "11":
view_transactions(data)
delete_transaction(data)
elif choice == "0":
print("\n Goodbye!\n")
break
else:
print(" Invalid choice.")
# ============================================================
# RUN
# ============================================================
if __name__ == "__main__":
main()
No comments:
Post a Comment