A simple Python expense tracking application that lets you add, store, analyze, and display expenses — built entirely with fundamental Python concepts.
This project fulfills the Expense Tracking System requirements:
| Requirement | Status |
|---|---|
| Simulated database (global list) | Done |
| Add expense with validation | Done |
| Calculate total expenses | Done |
| Calculate total by category | Done |
| Show all expenses | Done |
| Testing section with invalid input | Done |
python expense_tracking_system.pyExpected Output:
Error: Amount must be greater than 0.
Total Expenses: 170
Total Food Expenses: 150
All Expenses:
1. Food - Groceries : $50
2. Transport - Taxi : $20
3. Food - Restaurant : $100
expenses = []- A global list serves as the in-memory database for the entire application.
- Every expense added by
add_expense()is appended here as a dictionary. - Using a global list keeps things simple — all functions can read from and write to the same shared data store without needing to pass it around as an argument.
def add_expense(amount: float, category: str, description: str) -> dict:
if amount <= 0:
raise ValueError("Amount must be greater than 0.")
expense = {
"amount": amount,
"category": category,
"description": description
}
expenses.append(expense)
return expenseStep-by-step breakdown:
- Validation — The very first thing the function does is check whether
amountis positive. If not, it raises aValueErrorwith a descriptive message. This prevents bad data from entering the system. - Dictionary creation — A dictionary is built with three keys (
amount,category,description). Dictionaries are ideal here because they let us access each field by name rather than by position. - Storing — The dictionary is appended to the global
expenseslist. - Return value — The created expense is returned so the caller can confirm what was stored or use it further.
Key concepts demonstrated:
- Input validation with an
ifguard andraise - Exception handling via
ValueError - Type hints (
amount: float,-> dict) for readability - Dictionaries as structured data containers
def calculate_total_expenses() -> float:
total = 0
for expense in expenses:
total += expense["amount"]
return totalStep-by-step breakdown:
- Initialize accumulator —
totalstarts at0. - Loop — A
forloop iterates over every dictionary in theexpenseslist. - Accumulate — Each expense's
"amount"value is added tototalusing+=. - Return — The final sum is returned.
Key concepts demonstrated:
- Accumulator pattern — a common technique where you initialize a variable before a loop and update it on every iteration.
- Dictionary key access —
expense["amount"]retrieves the value stored under the"amount"key.
def calculate_total_by_category(category: str) -> float:
total = 0
for expense in expenses:
if expense["category"].lower() == category.lower():
total += expense["amount"]
return totalStep-by-step breakdown:
- Same accumulator setup as
calculate_total_expenses(). - Filtering — Inside the loop, an
ifcondition checks whether the expense's category matches the requested category. - Case-insensitive comparison — Both sides are converted to lowercase with
.lower()so that"Food","food", and"FOOD"all match. - Only matching expenses contribute to
total.
Key concepts demonstrated:
- Filtering inside a loop — combining iteration with a conditional check.
- String method
.lower()— ensures the comparison is not affected by capitalization differences. - Parameterized function — the
categoryargument makes this function reusable for any category.
def show_expenses() -> None:
if not expenses:
print("No expenses recorded.")
return
print("\nAll Expenses:")
for index, expense in enumerate(expenses, start=1):
print(
f"{index}. {expense['category']} - "
f"{expense['description']} : ${expense['amount']}"
)Step-by-step breakdown:
- Guard clause — If the list is empty (
not expensesevaluates toTruefor an empty list), a message is printed and the function returns early. This avoids printing a heading with no data below it. enumerate()withstart=1— Produces pairs of(index, expense)where the index begins at 1 instead of the default 0, making the output human-friendly.- f-string formatting — An f-string assembles each line, pulling values from the dictionary by key.
Key concepts demonstrated:
- Guard clause / early return — a clean pattern to handle edge cases at the top of a function.
enumerate()— a built-in that pairs each item with its index, removing the need for a manual counter variable.- f-strings — Python's modern string formatting syntax for embedding expressions inside strings.
def run_tests() -> None:
try:
add_expense(50, "Food", "Groceries")
add_expense(20, "Transport", "Taxi")
add_expense(100, "Food", "Restaurant")
add_expense(0, "Entertainment", "Cinema") # Invalid — triggers ValueError
except ValueError as error:
print("Error:", error)
print("\nTotal Expenses:", calculate_total_expenses())
print("Total Food Expenses:", calculate_total_by_category("Food"))
show_expenses()
if __name__ == "__main__":
run_tests()Step-by-step breakdown:
try/except— The fouradd_expense()calls are wrapped in atryblock. The first three succeed, but the fourth passes0as the amount, which triggers theValueErrorraised insideadd_expense(). Execution jumps to theexceptblock where the error message is printed.- Important behavior — Because the exception occurs on the fourth call, only three expenses are stored. The invalid one is never appended to the list.
- Aggregation calls —
calculate_total_expenses()returns170(50 + 20 + 100) andcalculate_total_by_category("Food")returns150(50 + 100). - Display —
show_expenses()prints all three valid expenses. if __name__ == "__main__":— This guard ensuresrun_tests()only executes when the script is run directly. If another module imports this file, the tests won't run automatically.
Key concepts demonstrated:
- Exception handling with
try/except— catching specific exceptions (ValueError) gracefully. as error— captures the exception object so its message can be printed.__name__guard — standard Python practice to separate reusable code from script execution.
| Concept | Where Used |
|---|---|
| Global variables | expenses list shared across all functions |
| Dictionaries | Each expense stored as {"amount", "category", "description"} |
| Lists | expenses list holds all expense dictionaries |
| Functions with parameters | All four core functions accept arguments and return values |
| Type hints | Function signatures use : float, : str, -> dict, etc. |
| Docstrings | Every function has a triple-quoted description |
| Input validation | amount <= 0 check with raise ValueError |
| Exception handling | try / except ValueError in run_tests() |
for loop |
Iterating over the expenses list |
| Accumulator pattern | total += expense["amount"] in calculation functions |
enumerate() |
Numbering expenses in show_expenses() |
| f-strings | Formatting output strings |
.lower() comparison |
Case-insensitive category matching |
| Guard clause | Early return in show_expenses() for empty list |
__name__ guard |
Running tests only when script is executed directly |