import mysql.
connector
import csv
import sys # Import sys to allow exiting the application cleanly
# --- Database Setup ---
def setup_database():
"""
Connects to MySQL and sets up the 'expense_db' database and necessary
tables
for users and expenses if they don't already exist.
"""
conn = None # Initialize conn to None
cursor = None # Initialize cursor to None
try:
# Establish connection to MySQL server (without specifying a database
initially)
conn = mysql.connector.connect(
host="localhost",
user="root",
password="1234" # IMPORTANT: Replace with your actual MySQL root
password
cursor = conn.cursor()
# Create the database if it doesn't exist
cursor.execute("CREATE DATABASE IF NOT EXISTS expense_db")
print("✅ Database 'expense_db' checked/created.")
# Switch to the newly created or existing database
cursor.execute("USE expense_db")
print("✅ Switched to 'expense_db'.")
# Create 'users' table if it doesn't exist
cursor.execute("""
CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
password VARCHAR(50) NOT NULL
""")
print("✅ Table 'users' checked/created.")
# Create 'expenses' table if it doesn't exist
cursor.execute("""
CREATE TABLE IF NOT EXISTS expenses (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT,
date DATE NOT NULL,
category VARCHAR(100) NOT NULL,
amount FLOAT NOT NULL,
description TEXT,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
""")
print("✅ Table 'expenses' checked/created.")
conn.commit()
print("✅ MySQL database and tables set up successfully.")
except mysql.connector.Error as err:
print(f"❌ Error setting up database: {err}")
print("Please ensure MySQL is running and your 'root' user password is
correct in the script.")
finally:
if cursor:
cursor.close()
if conn and conn.is_connected():
conn.close()
# --- MySQL Connection ---
def connect():
"""
Establishes a connection to the 'expense_db' database.
Returns the connection object.
"""
try:
return mysql.connector.connect(
host="localhost",
user="root",
password="your_mysql_password", # IMPORTANT: Replace with your
actual MySQL root password
database="expense_db"
except mysql.connector.Error as err:
print(f"❌ Database connection error: {err}")
print("Please ensure MySQL is running, the 'expense_db' exists, and
connection details are correct.")
return None
# --- User Authentication ---
def register():
"""
Registers a new user by taking username and password input.
Checks for duplicate usernames.
"""
username = input("New username: ").strip()
password = input("New password: ").strip()
if not username:
print("❌ Username cannot be empty.")
return
if not password:
print("❌ Password cannot be empty.")
return
conn = connect()
if not conn:
return
cursor = conn.cursor()
try:
cursor.execute("INSERT INTO users (username, password) VALUES (%s,
%s)", (username, password))
conn.commit()
print(f"✅ User '{username}' registered successfully.")
except mysql.connector.IntegrityError:
print("❌ Username already exists. Please choose a different username.")
except mysql.connector.Error as err:
print(f"❌ Error during registration: {err}")
finally:
if cursor:
cursor.close()
if conn and conn.is_connected():
conn.close()
def login():
"""
Authenticates a user by checking provided username and password.
Returns the user's ID if successful, otherwise None.
"""
username = input("Username: ").strip()
password = input("Password: ").strip()
if not username or not password:
print("❌ Username and password cannot be empty.")
return None
conn = connect()
if not conn:
return None
cursor = conn.cursor()
try:
cursor.execute("SELECT id FROM users WHERE username = %s AND
password = %s", (username, password))
user = cursor.fetchone()
if user:
print(f"✅ Logged in as {username}.")
return user[0] # Return user_id
else:
print("❌ Invalid username or password.")
return None
except mysql.connector.Error as err:
print(f"❌ Error during login: {err}")
return None
finally:
if cursor:
cursor.close()
if conn and conn.is_connected():
conn.close()
# --- Expense Functions ---
def add_expense(user_id):
"""
Adds a new expense record for the logged-in user.
"""
date = input("Enter date (YYYY-MM-DD): ").strip()
category = input("Enter category: ").strip()
amount_str = input("Enter amount: ").strip()
description = input("Enter description (optional): ").strip()
if not date:
print("❌ Date cannot be empty.")
return
if not category:
print("❌ Category cannot be empty.")
return
try:
amount = float(amount_str)
if amount <= 0:
print("❌ Amount must be a positive number.")
return
except ValueError:
print("❌ Invalid amount. Please enter a number (e.g., 50.00).")
return
conn = connect()
if not conn:
return
cursor = conn.cursor()
try:
cursor.execute(
"INSERT INTO expenses (user_id, date, category, amount, description)
VALUES (%s, %s, %s, %s, %s)",
(user_id, date, category, amount, description)
conn.commit()
print("✅ Expense added successfully.")
except mysql.connector.Error as err:
print(f"❌ Error adding expense: {err}")
finally:
if cursor:
cursor.close()
if conn and conn.is_connected():
conn.close()
def view_expenses(user_id):
"""
Displays all expenses for the logged-in user, ordered by date.
"""
conn = connect()
if not conn:
return
cursor = conn.cursor()
try:
cursor.execute("SELECT id, date, category, amount, description FROM
expenses WHERE user_id = %s ORDER BY date DESC", (user_id,))
rows = cursor.fetchall()
print("\n--- Your Expenses ---")
if not rows:
print("No expenses recorded yet.")
return
print("{:<5} {:<12} {:<15} {:<10} {}".format("ID", "Date", "Category",
"Amount", "Description"))
print("-" * 60)
for row in rows:
# Format amount to two decimal places for display, handle None for
description
print("{:<5} {:<12} {:<15} {:<10.2f} {}".format(row[0], str(row[1]), row[2],
row[3], row[4] or ''))
except mysql.connector.Error as err:
print(f"❌ Error viewing expenses: {err}")
finally:
if cursor:
cursor.close()
if conn and conn.is_connected():
conn.close()
def update_expense(user_id):
"""
Updates an existing expense record for the logged-in user.
Allows partial updates by leaving fields blank.
"""
expense_id_str = input("Enter the ID of the expense to update: ").strip()
try:
expense_id = int(expense_id_str)
except ValueError:
print("❌ Invalid expense ID. Please enter a number.")
return
# First, check if the expense exists and belongs to the user
conn_check = connect()
if not conn_check:
return
cursor_check = conn_check.cursor()
try:
cursor_check.execute("SELECT id FROM expenses WHERE id = %s AND
user_id = %s", (expense_id, user_id))
if not cursor_check.fetchone():
print("❌ Expense not found or you don't have permission to update it.")
return
except mysql.connector.Error as err:
print(f"❌ Error checking expense: {err}")
return
finally:
if cursor_check:
cursor_check.close()
if conn_check and conn_check.is_connected():
conn_check.close()
print(f"\nEnter new details for expense ID {expense_id} (leave blank to keep
current value):")
new_date = input(f"New date (YYYY-MM-DD): ").strip()
new_category = input(f"New category: ").strip()
new_amount_str = input(f"New amount: ").strip()
new_description = input(f"New description: ").strip()
conn = connect()
if not conn:
return
cursor = conn.cursor()
try:
updates = []
params = []
if new_date:
updates.append("date = %s")
params.append(new_date)
if new_category:
updates.append("category = %s")
params.append(new_category)
if new_amount_str:
try:
new_amount = float(new_amount_str)
if new_amount <= 0:
print("❌ Amount must be a positive number. Update cancelled.")
return # Exit if amount is invalid
updates.append("amount = %s")
params.append(new_amount)
except ValueError:
print("❌ Invalid amount. Please enter a number (e.g., 100.50). Update
cancelled.")
return # Exit if amount is invalid
if new_description:
updates.append("description = %s")
params.append(new_description)
if not updates:
print("❗ No new information provided. Nothing to update.")
return
query = f"UPDATE expenses SET {', '.join(updates)} WHERE id = %s AND
user_id = %s"
params.extend([expense_id, user_id])
cursor.execute(query, tuple(params))
conn.commit()
if cursor.rowcount > 0:
print(f"✅ Expense ID {expense_id} updated successfully.")
else:
print("❗ No changes were made, or expense was not found/owned by
you (this shouldn't happen after the initial check).")
except mysql.connector.Error as err:
print(f"❌ Error updating expense: {err}")
finally:
if cursor:
cursor.close()
if conn and conn.is_connected():
conn.close()
def delete_expense(user_id):
"""
Deletes an expense record for the logged-in user.
Includes a confirmation step.
"""
expense_id_str = input("Enter the ID of the expense to delete: ").strip()
try:
expense_id = int(expense_id_str)
except ValueError:
print("❌ Invalid expense ID. Please enter a number.")
return
conn = connect()
if not conn:
return
cursor = conn.cursor()
try:
# Check if the expense exists and belongs to the user before asking for
confirmation
cursor.execute("SELECT id FROM expenses WHERE id = %s AND user_id =
%s", (expense_id, user_id))
if not cursor.fetchone():
print("❌ Expense not found or you don't have permission to delete it.")
return
confirm = input(f"Are you sure you want to delete expense ID
{expense_id}? This cannot be undone. (y/n): ").strip().lower()
if confirm != 'y':
print("Deletion cancelled.")
return
cursor.execute("DELETE FROM expenses WHERE id = %s AND user_id =
%s", (expense_id, user_id))
conn.commit()
if cursor.rowcount > 0:
print(f" Expense ID {expense_id} deleted successfully.")
else:
# This case should ideally not be reached if the initial check passed
print("❗ Expense not found or no changes were applied.")
except mysql.connector.Error as err:
print(f"❌ Error deleting expense: {err}")
finally:
if cursor:
cursor.close()
if conn and conn.is_connected():
conn.close()
# --- Reporting and Analysis Functions ---
def category_summary(user_id):
"""
Provides a summary of total spending per category for the logged-in user.
"""
conn = connect()
if not conn:
return
cursor = conn.cursor()
try:
cursor.execute("SELECT category, SUM(amount) FROM expenses WHERE
user_id = %s GROUP BY category", (user_id,))
rows = cursor.fetchall()
print("\n--- Category-wise Summary ---")
if not rows:
print("No expenses recorded to summarize by category.")
return
for row in rows:
print("Category: {:<15} Total: ₹{:.2f}".format(row[0], row[1]))
except mysql.connector.Error as err:
print(f"❌ Error generating category summary: {err}")
finally:
if cursor:
cursor.close()
if conn and conn.is_connected():
conn.close()
def monthly_report(user_id):
"""
Generates a report of expenses for a specific month (YYYY-MM) for the
logged-in user.
"""
month = input("Enter month for report (YYYY-MM): ").strip()
# Basic validation for month format
if not (len(month) == 7 and month[4] == '-' and month[:4].isdigit() and
month[5:].isdigit()):
print("❌ Invalid month format. Please use YYYY-MM (e.g., 2023-10).")
return
conn = connect()
if not conn:
return
cursor = conn.cursor()
try:
# Use DATE_FORMAT to compare only year and month
cursor.execute("SELECT id, date, category, amount, description FROM
expenses WHERE user_id = %s AND DATE_FORMAT(date, '%%Y-%%m') = %s
ORDER BY date ASC", (user_id, month))
rows = cursor.fetchall()
print(f"\n--- Monthly Report for {month} ---")
if not rows:
print(f"No expenses recorded for {month}.")
return
print("{:<5} {:<12} {:<15} {:<10} {}".format("ID", "Date", "Category",
"Amount", "Description"))
print("-" * 60)
total_monthly_spent = 0
for row in rows:
print("{:<5} {:<12} {:<15} {:<10.2f} {}".format(row[0], str(row[1]), row[2],
row[3], row[4] or ''))
total_monthly_spent += row[3]
print("-" * 60)
print(f"Total spent in {month}: ₹{total_monthly_spent:.2f}")
except mysql.connector.Error as err:
print(f"❌ Error generating monthly report: {err}")
finally:
if cursor:
cursor.close()
if conn and conn.is_connected():
conn.close()
def export_to_csv(user_id):
"""
Exports all expenses for the logged-in user to a CSV file.
"""
filename = input("Enter desired CSV filename (e.g., my_expenses.csv):
").strip()
if not filename:
print("❌ Filename cannot be empty.")
return
if not filename.lower().endswith(".csv"):
filename += ".csv"
conn = connect()
if not conn:
return
cursor = conn.cursor()
try:
cursor.execute("SELECT id, date, category, amount, description FROM
expenses WHERE user_id = %s ORDER BY date ASC", (user_id,))
rows = cursor.fetchall()
if not rows:
print("No expenses to export.")
return
with open(filename, mode='w', newline='', encoding='utf-8') as file:
writer = csv.writer(file)
writer.writerow(['ID', 'Date', 'Category', 'Amount', 'Description'])
for row in rows:
writer.writerow([row[0], str(row[1]), row[2], row[3], row[4] or ''])
print(f"✅ Data exported to '{filename}' successfully.")
except IOError as io_err:
print(f"❌ Error writing to file '{filename}': {io_err}")
print("Please check if you have write permissions in the current
directory.")
except mysql.connector.Error as db_err:
print(f"❌ Error fetching data for export: {db_err}")
finally:
if cursor:
cursor.close()
if conn and conn.is_connected():
conn.close()
def analyze_expenses(user_id):
"""
Analyzes expense patterns and provides suggestions.
Identifies the highest spending category and offers a tip if spending is high in
one category.
"""
conn = connect()
if not conn:
return
cursor = conn.cursor()
try:
cursor.execute("SELECT category, SUM(amount) as total FROM expenses
WHERE user_id = %s GROUP BY category ORDER BY total DESC", (user_id,))
rows = cursor.fetchall()
if not rows:
print("No expenses to analyze. Add some expenses first!")
return
print("\n--- Expense Analysis ---")
total_spent = sum([row[1] for row in rows])
if total_spent == 0:
print("No spending recorded yet to analyze.")
return
print(f"Total spending across all categories: ₹{total_spent:.2f}")
print("\nSpending by Category:")
for row in rows:
percentage = (row[1] / total_spent) * 100
print(f"- {row[0]}: ₹{row[1]:.2f} ({percentage:.1f}%)")
max_category = rows[0] # Already ordered by total DESC
print(f"\n🔍 Your highest spending category is: **{max_category[0]}** (₹
{max_category[1]:.2f})")
# Simple suggestion logic
if max_category[1] / total_spent > 0.4: # If more than 40% of total is in one
category
print("💡 Suggestion: Consider reviewing your expenses in this category.
Small adjustments here can significantly impact your overall budget.")
elif max_category[1] / total_spent > 0.2:
print("👍 Tip: Keep an eye on your spending in this category. Consistent
tracking can help identify areas for saving.")
else:
print("📊 Your spending seems well-distributed across categories. Great
job!")
except mysql.connector.Error as err:
print(f"❌ Error analyzing expenses: {err}")
finally:
if cursor:
cursor.close()
if conn and conn.is_connected():
conn.close()
# --- Main Program ---
def main():
"""
Main function to run the expense tracker application.
Handles user login, registration, and navigates the main menu.
"""
print("Initializing database...")
setup_database()
while True:
print("\n===== EXPENSE TRACKER =====")
print("1. Register")
print("2. Login")
print("3. Exit")
option = input("Choose an option: ").strip()
if option == '1':
register()
elif option == '2':
user_id = login()
if user_id:
# User is logged in, show main expense menu
while True:
print("\n--- Main Menu ---")
print("1. Add Expense")
print("2. View Expenses")
print("3. Update Expense")
print("4. Delete Expense")
print("5. Category Summary")
print("6. Monthly Report")
print("7. Export to CSV")
print("8. Analyze & Suggest")
print("9. Logout")
choice = input("Enter your choice: ").strip()
if choice == '1':
add_expense(user_id)
elif choice == '2':
view_expenses(user_id)
elif choice == '3':
update_expense(user_id)
elif choice == '4':
delete_expense(user_id)
elif choice == '5':
category_summary(user_id)
elif choice == '6':
monthly_report(user_id)
elif choice == '7':
export_to_csv(user_id)
elif choice == '8':
analyze_expenses(user_id)
elif choice == '9':
print("Logging out...")
break # Break out of the inner loop to return to login/register
menu
else:
print("Invalid choice. Please try again.")
elif option == '3':
print("Goodbye! 👋")
sys.exit() # Exit the program cleanly
else:
print("Invalid option. Please choose 1, 2, or 3.")
if __name__ == "__main__":
main()