Create Expense Tracker Project in Python

FREE Online Courses: Dive into Knowledge for Free. Learn More!

In this Python project, we will build a GUI-based Expense Tracker using the Tkinter, Tkcalender, and SQLite libraries and the message box and Ttk modules of the Tkinter library. It is an intermediate-level project, where you will learn a lot about databases and GUIs and apply them in real life. Let’s start!

About Expense Trackers:

An expense tracker is a desktop application that keeps track of all your expenses and stores all the information regarding them, including the person to whom you have paid the money (also called payee) and the reason why you paid the money.

About the project:

The objective of this project is to create a GUI based Expense Tracker. To build this, you will need an intermediate understanding of the Tkinter library, SQL language and its commands, and basic understanding of messagebox module, ttk.Treeview widget and tkcalender library.

Project Prerequisites:

To build this project, we will need the following libraries:

1. Tkinter – To create the GUI.

a. messagebox – To display a box containing information, warning, or error or asking a question.

b. Ttk.Treeview – To display a table in the GUI window.

2. Tkcalender.DateEntry – To enter a date.

3. SQLite – To connect the Python script to the SQL database.

Only the Tkinter library comes pre-installed with Python, so you need to run the following command in your terminal to install them:

python -m pip install tkcalender sqlite

Download Python Expense Tracker Project code

You can download python source code for the expense tracker from the following link: Expense Tracker Project Code

Project File Structure:

Here are the steps you will need to execute to build this project:

1. Importing the necessary modules and libraries
2. Connecting to the database and creating the GUI window
3. Creating the database and data manipulation functions

Let’s take a closer look at these steps:

1. Importing the necessary modules and libraries:

import datetime
import sqlite3
from tkcalendar import DateEntry

from tkinter import *
import tkinter.messagebox as mb
import tkinter.ttk as ttk

Explanation:

In this step, we will import all the libraries and modules that we will use to create this project.

2. Connecting to the database and creating the GUI window:

# Connecting to the Database
connector = sqlite3.connect("Expense Tracker.db")
cursor = connector.cursor()

connector.execute(
  'CREATE TABLE IF NOT EXISTS ExpenseTracker (ID INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, Date DATETIME, Payee TEXT, Description TEXT, Amount FLOAT, ModeOfPayment TEXT)'
)
connector.commit()

# Backgrounds and Fonts
dataentery_frame_bg = 'Red'
buttons_frame_bg = 'Tomato'
hlb_btn_bg = 'IndianRed'

lbl_font = ('Georgia', 13)
entry_font = 'Times 13 bold'
btn_font = ('Gill Sans MT', 13)

# Initializing the GUI window
root = Tk()
root.title('PythonGeeks Expense Tracker')
root.geometry('1200x550')
root.resizable(0, 0)

Label(root, text='EXPENSE TRACKER', font=('Noto Sans CJK TC', 15, 'bold'), bg=hlb_btn_bg).pack(side=TOP, fill=X)

# StringVar and DoubleVar variables
desc = StringVar()
amnt = DoubleVar()
payee = StringVar()
MoP = StringVar(value='Cash')

# Frames
data_entry_frame = Frame(root, bg=dataentery_frame_bg)
data_entry_frame.place(x=0, y=30, relheight=0.95, relwidth=0.25)

buttons_frame = Frame(root, bg=buttons_frame_bg)
buttons_frame.place(relx=0.25, rely=0.05, relwidth=0.75, relheight=0.21)

tree_frame = Frame(root)
tree_frame.place(relx=0.25, rely=0.26, relwidth=0.75, relheight=0.74)

# Data Entry Frame
Label(data_entry_frame, text='Date (M/DD/YY) :', font=lbl_font, bg=dataentery_frame_bg).place(x=10, y=50)
date = DateEntry(data_entry_frame, date=datetime.datetime.now().date(), font=entry_font)
date.place(x=160, y=50)

Label(data_entry_frame, text='Payee\t             :', font=lbl_font, bg=dataentery_frame_bg).place(x=10, y=230)
Entry(data_entry_frame, font=entry_font, width=31, text=payee).place(x=10, y=260)

Label(data_entry_frame, text='Description           :', font=lbl_font, bg=dataentery_frame_bg).place(x=10, y=100)
Entry(data_entry_frame, font=entry_font, width=31, text=desc).place(x=10, y=130)

Label(data_entry_frame, text='Amount\t             :', font=lbl_font, bg=dataentery_frame_bg).place(x=10, y=180)
Entry(data_entry_frame, font=entry_font, width=14, text=amnt).place(x=160, y=180)

Label(data_entry_frame, text='Mode of Payment:', font=lbl_font, bg=dataentery_frame_bg).place(x=10, y=310)
dd1 = OptionMenu(data_entry_frame, MoP, *['Cash', 'Cheque', 'Credit Card', 'Debit Card', 'Paytm', 'Google Pay', 'Razorpay'])
dd1.place(x=160, y=305)     ;     dd1.configure(width=10, font=entry_font)

Button(data_entry_frame, text='Add expense', command=add_another_expense, font=btn_font, width=30,
      bg=hlb_btn_bg).place(x=10, y=395)
Button(data_entry_frame, text='Convert to words before adding', font=btn_font, width=30, bg=hlb_btn_bg).place(x=10,y=450)

# Buttons' Frame
Button(buttons_frame, text='Delete Expense', font=btn_font, width=25, bg=hlb_btn_bg, command=remove_expense).place(x=30, y=5)

Button(buttons_frame, text='Clear Fields in DataEntry Frame', font=btn_font, width=25, bg=hlb_btn_bg,
      command=clear_fields).place(x=335, y=5)

Button(buttons_frame, text='Delete All Expenses', font=btn_font, width=25, bg=hlb_btn_bg, command=remove_all_expenses).place(x=640, y=5)

Button(buttons_frame, text='View Selected Expense\'s Details', font=btn_font, width=25, bg=hlb_btn_bg,
      command=view_expense_details).place(x=30, y=65)

Button(buttons_frame, text='Edit Selected Expense', command=edit_expense, font=btn_font, width=25, bg=hlb_btn_bg).place(x=335,y=65)

Button(buttons_frame, text='Convert Expense to a sentence', font=btn_font, width=25, bg=hlb_btn_bg,
      command=selected_expense_to_words).place(x=640, y=65)

# Treeview Frame
table = ttk.Treeview(tree_frame, selectmode=BROWSE, columns=('ID', 'Date', 'Payee', 'Description', 'Amount', 'Mode of Payment'))

X_Scroller = Scrollbar(table, orient=HORIZONTAL, command=table.xview)
Y_Scroller = Scrollbar(table, orient=VERTICAL, command=table.yview)
X_Scroller.pack(side=BOTTOM, fill=X)
Y_Scroller.pack(side=RIGHT, fill=Y)

table.config(yscrollcommand=Y_Scroller.set, xscrollcommand=X_Scroller.set)

table.heading('ID', text='S No.', anchor=CENTER)
table.heading('Date', text='Date', anchor=CENTER)
table.heading('Payee', text='Payee', anchor=CENTER)
table.heading('Description', text='Description', anchor=CENTER)
table.heading('Amount', text='Amount', anchor=CENTER)
table.heading('Mode of Payment', text='Mode of Payment', anchor=CENTER)

table.column('#0', width=0, stretch=NO)
table.column('#1', width=50, stretch=NO)
table.column('#2', width=95, stretch=NO)  # Date column
table.column('#3', width=150, stretch=NO)  # Payee column
table.column('#4', width=325, stretch=NO)  # Title column
table.column('#5', width=135, stretch=NO)  # Amount column
table.column('#6', width=125, stretch=NO)  # Mode of Payment column

table.place(relx=0, y=0, relheight=1, relwidth=1)

list_all_expenses()

# Finalizing the GUI window
root.update()
root.mainloop()

Explanation:

1. In this step, we will connect our Python script to the SQLite database. Here all the information will be stored and our GUI window.

2. To connect to the database,

  • We will create a connector instance using the sqlite.connector (<database name>) to create a database, activate it and perform functions in the database using the script.

3. To create our GUI window, we will first create a Tk object that will become our main window. Then we will set its basic attributes and move onto placing components and widgets in it.

4. Our first widget is the static text saying “Expense Tracker” (a Label widget) packed at the top of the window.

5. Next, we have three Frame widgets (widgets containers inside a window) that will contain different aspects of our GUI window.

6. The frame on the left side of the window contains Labels and Entry fields. Here the details about a new expense can be entered, or the details about an existing expense can be viewed. This frame also has two buttons, one that adds the expense to the database when pressed. The other can show how you read the expense, before you add it to the database.

7. The frame on the bottom-right side of the window contains a Treeview object. This acts as a table where the data from the database tables is displayed.

8. The frame on the top-right side of the window contains the buttons that can perform certain operations on the expense selected in the table, or the database as a whole such as editing the selected expense, converting it to words, delete it or delete the entire database.

3. Creating the database and data manipulation functions:

# Functions
def list_all_expenses():
  global connector, table

  table.delete(*table.get_children())

  all_data = connector.execute('SELECT * FROM ExpenseTracker')
  data = all_data.fetchall()

  for values in data:
     table.insert('', END, values=values)


def view_expense_details():
  global table
  global date, payee, desc, amnt, MoP

  if not table.selection():
     mb.showerror('No expense selected', 'Please select an expense from the table to view its details')

  current_selected_expense = table.item(table.focus())
  values = current_selected_expense['values']

  expenditure_date = datetime.date(int(values[1][:4]), int(values[1][5:7]), int(values[1][8:]))

  date.set_date(expenditure_date) ; payee.set(values[2]) ; desc.set(values[3]) ; amnt.set(values[4]) ; MoP.set(values[5])


def clear_fields():
  global desc, payee, amnt, MoP, date, table

  today_date = datetime.datetime.now().date()

  desc.set('') ; payee.set('') ; amnt.set(0.0) ; MoP.set('Cash'), date.set_date(today_date)
  table.selection_remove(*table.selection())


def remove_expense():
  if not table.selection():
     mb.showerror('No record selected!', 'Please select a record to delete!')
     return

  current_selected_expense = table.item(table.focus())
  values_selected = current_selected_expense['values']

  surety = mb.askyesno('Are you sure?', f'Are you sure that you want to delete the record of {values_selected[2]}')

  if surety:
     connector.execute('DELETE FROM ExpenseTracker WHERE ID=%d' % values_selected[0])
     connector.commit()

     list_all_expenses()
     mb.showinfo('Record deleted successfully!', 'The record you wanted to delete has been deleted successfully')


def remove_all_expenses():
  surety = mb.askyesno('Are you sure?', 'Are you sure that you want to delete all the expense items from the database?', icon='warning')

  if surety:
     table.delete(*table.get_children())

     connector.execute('DELETE FROM ExpenseTracker')
     connector.commit()

     clear_fields()
     list_all_expenses()
     mb.showinfo('All Expenses deleted', 'All the expenses were successfully deleted')
  else:
     mb.showinfo('Ok then', 'The task was aborted and no expense was deleted!')


def add_another_expense():
  global date, payee, desc, amnt, MoP
  global connector

  if not date.get() or not payee.get() or not desc.get() or not amnt.get() or not MoP.get():
     mb.showerror('Fields empty!', "Please fill all the missing fields before pressing the add button!")
  else:
     connector.execute(
     'INSERT INTO ExpenseTracker (Date, Payee, Description, Amount, ModeOfPayment) VALUES (?, ?, ?, ?, ?)',
     (date.get_date(), payee.get(), desc.get(), amnt.get(), MoP.get())
     )
     connector.commit()

     clear_fields()
     list_all_expenses()
     mb.showinfo('Expense added', 'The expense whose details you just entered has been added to the database')


def edit_expense():
  global table

  def edit_existing_expense():
     global date, amnt, desc, payee, MoP
     global connector, table

     current_selected_expense = table.item(table.focus())
     contents = current_selected_expense['values']

     connector.execute('UPDATE ExpenseTracker SET Date = ?, Payee = ?, Description = ?, Amount = ?, ModeOfPayment = ? WHERE ID = ?',
                       (date.get_date(), payee.get(), desc.get(), amnt.get(), MoP.get(), contents[0]))
     connector.commit()

     clear_fields()
     list_all_expenses()

     mb.showinfo('Data edited', 'We have updated the data and stored in the database as you wanted')
     edit_btn.destroy()
     return

  if not table.selection():
     mb.showerror('No expense selected!', 'You have not selected any expense in the table for us to edit; please do that!')
     return

  view_expense_details()

  edit_btn = Button(data_entry_frame, text='Edit expense', font=btn_font, width=30,
                    bg=hlb_btn_bg, command=edit_existing_expense)
  edit_btn.place(x=10, y=395)


def selected_expense_to_words():
  global table

  if not table.selection():
     mb.showerror('No expense selected!', 'Please select an expense from the table for us to read')
     return

  current_selected_expense = table.item(table.focus())
  values = current_selected_expense['values']

  message = f'Your expense can be read like: \n"You paid {values[4]} to {values[2]} for {values[3]} on {values[1]} via {values[5]}"'

  mb.showinfo('Here\'s how to read your expense', message)


def expense_to_words_before_adding():
  global date, desc, amnt, payee, MoP

  if not date or not desc or not amnt or not payee or not MoP:
     mb.showerror('Incomplete data', 'The data is incomplete, meaning fill all the fields first!')

  message = f'Your expense can be read like: \n"You paid {amnt.get()} to {payee.get()} for {desc.get()} on {date.get_date()} via {MoP.get()}"'

  add_question = mb.askyesno('Read your record like: ', f'{message}\n\nShould I add it to the database?')

  if add_question:
     add_another_expense()
  else:
     mb.showinfo('Ok', 'Please take your time to add this record')

Explanation:

  • In this step, we will create all the functions for all the buttons on our Expense Tracker window.
  • The one common thing in all the functions is that we will make the scope of all the Entry fields and the database connector in the left frame global by using the global keyword.
  • One common thing in all the functions that perform an operation regarding one single expense is that if there is no selection in the table, it will display an error message using the mb.showerror() function.
  • Another common thing in all the functions that deal with data are two statements, connector.execute() and connector.commit(). The first statement takes an argument, which is supposed to be an SQL command, is used to execute that argument as in an SQL engine and the second one is used to finalize that change and perform it in the database.
  • In the list_all_expenses() function, we will delete all the children of the table, then get all the data from the database table using the connector’s SELECT command and then we will insert all of it to the table using the .insert() method of it.
  • In the view_expense_details(), we will get the values in the currently selected item of the table. Then set the values of the StringVar, IntVar or DateEntry variables to the corresponding indices of the tuple containing the values of the said selected item.
  • In the clear_fields() function, we will set all the StringVar variables to an empty string and the DataEntry variable to the current date.
  • In the remove_expense() function, we will get the values of the currently selected item in the table, and ask the user if he really wants to delete it. If he/she wants to delete it, we will delete it using the DELETE command in the execute-commit method combo of the connector.
  • In the remove_all_expenses() function, we will first ask the user if they are sure that they want to delete all the expenses that we have stored for them using the messagebox.askyesno() function. If they reply yes, we will delete using the DELETE * FROM <tablename> command as an argument to the connector.execute() and the connector.commit() methods.
  • In the add_another_expense() function, if any of the Entry fields is empty, we will display an error message box, otherwise we will enter the INSERT command in the connector’s execute-commit methods combination, then clear all the entry fields and display the updated table.
  • The edit_expense() function only makes changes in the components present on the window. However, with the nested edit_existing_expense() function, we get the contents of the selected record in the table and then use the UPDATE command to edit the data in the table with the help of the ID of the expense, which the user cannot edit.
  • In the selected_words_to_expense() function, we will simply check if there is a selection in the table. If there is, we will display an information box with the message telling the user how to read the expense using the mb.showinfo() function.
  • The expense_to_words_before_adding() function is to tell the user how to read their expense before they add it to the database. This is to make sure that they are confident with the data entered, and how it will be interpreted by others. In that function, we will take the contents of the Entry fields and do something similar to the previous function, except the information box will now be a ask-yes-or-no box where, if the user presses yes, the expense will be added to the database.

Python Expense Tracker Output

python expense tracker output

Summary

Congratulations! You have now created your own Expense Tracker using the Tkinter, SQLite and Tkcalender libraries.

You can use this project to keep track of your own expenses rather than using a paper that you might misplace to keep track of them, and many more.

You give me 15 seconds I promise you best tutorials
Please share your happy experience on Google | Facebook


6 Responses

  1. Sarika says:

    Python programming language

  2. Mani says:

    Can I get the project design, application architecture, DFD diagram, database structure

  3. Mani says:

    Can I get full project report

  4. Anushka says:

    executed this code but all the ‘buttons’ created using functions did not show up on the expense tracker. No error in the code either.

  5. Aarti says:

    give the steps to execute this project give the steps

  6. cheshta says:

    Give the steps for the execution of this source code

Leave a Reply

Your email address will not be published. Required fields are marked *