Project: Expense Tracker with Tests

Python 3.14 ยท โœ“ verified by execution on 2026-07-29

Welcome to the Expense Tracker project! In this lesson, we will build a real-world Python application using Test-Driven Development (TDD).

The Goal

We will track expenses using @dataclass, store them using json and pathlib, and ensure everything works with unittest.

Visualizing Dataclass Creation

Here is how a dataclass is created in memory over time.

python ยท visualize
from dataclasses import dataclass
@dataclass
class Expense:
    amount: float
    desc: str

e = Expense(10.5, 'Coffee')

Example: ex-unittest

Creating and running a basic unit test.

python
import unittest, io, re, sys
class TestExpense(unittest.TestCase):
    def test_basic(self):
        self.assertEqual(1, 1)
if __name__ == '__main__':
    out = io.StringIO()
    unittest.main(exit=False, testRunner=unittest.TextTestRunner(stream=out, verbosity=0))
    print(re.sub(r'in \d+\.\d+s', 'in 0.000s', out.getvalue().strip()))
Output
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

Example: ex-json-dump

Serializing a dictionary to a JSON formatted string.

python
import json
data = {'amount': 10.5, 'desc': 'Coffee'}
print(json.dumps(data))
Output
{"amount": 10.5, "desc": "Coffee"}

Example: ex-json-load

Deserializing a JSON file back into a Python object.

python
import json
import tempfile
with tempfile.NamedTemporaryFile('w+', delete=False) as f:
    f.write('{"amount": 10.5, "desc": "Coffee"}')
    f_name = f.name
with open(f_name, 'r') as f:
    print(json.load(f)['desc'])
import os
os.remove(f_name)
Output
Coffee

Example: ex-dataclass

Creating a simple dataclass to hold expense data.

python
from dataclasses import dataclass
@dataclass
class Expense:
    amount: float
    desc: str
e = Expense(10.5, 'Coffee')
print(e)
Output
Expense(amount=10.5, desc='Coffee')

Example: ex-pathlib

Using Pathlib to represent a file path.

python
from pathlib import Path
p = Path('expenses.json')
print(p.name)
Output
expenses.json

Example: ex-datetime

Using datetime to format a date string.

python
from datetime import datetime
d = datetime(2026, 7, 29)
print(d.strftime('%Y-%m-%d'))
Output
2026-07-29

BugHunt / Predict the Output: The following code tries to serialize an Expense dataclass directly to JSON, but it crashes. What exception do you think it raises?

Click here to make your guess

It raises a TypeError! Custom objects like dataclasses cannot be directly serialized by JSON. You must convert it to a dictionary first using dataclasses.asdict().

Check yourself

You try to json.dump() a dataclass instance and get a TypeError. Why?

Reveal answer

JSON only handles strings, numbers, booleans, lists and dicts โ€” convert to a dict first โ€” json only knows the basic JSON types. Use dataclasses.asdict(obj) (or a custom default=) to turn the object into a dict first.

You wrote a TestCase class but running the file prints nothing. What is missing?

Reveal answer

unittest.main(), or running it through a test runner โ€” Defining tests does not run them. unittest.main() (or python -m unittest) discovers and executes them.

Does @dataclass enforce the type hints you write on its fields?

Reveal answer

No โ€” hints define the structure, but nothing is checked at runtime โ€” Type hints are used to generate __init__ and friends, not to validate. Passing the wrong type succeeds silently unless you add checks.

What is the difference between json.load and json.loads?

Reveal answer

load reads from a file object; loads parses a string โ€” The trailing s means string. json.load(f) takes an open file; json.loads(text) takes a str.

Challenges

Challenge 1 +20 XP

Create an Expense dataclass with amount (float), category (str), and date (str). Instantiate one for a 15.50 'Food' expense on '2026-07-29' and print it.

python
  • Test 1 โ€” expects "Expense(amount=15.5, category='Food', date='2026-07-29')"
Need a hint? (โˆ’25% XP)

Use @dataclass and define the fields with their types. Then create the object and print it.

Show solution (0 XP)
from dataclasses import dataclass

@dataclass
class Expense:
    amount: float
    category: str
    date: str

e = Expense(15.50, 'Food', '2026-07-29')
print(e)

Challenge 2 +20 XP

Write a unittest TestCase named TestMath with a method test_add that asserts 1 + 1 equals 2. Run the tests using unittest.main(exit=False, argv=['']).

python
  • Test 1 โ€” expects "----------------------------------------------------------------------\nRan 1 test in 0.000s\n\nOK"
Need a hint? (โˆ’25% XP)

Subclass unittest.TestCase, define a test method starting with test_, and use self.assertEqual.

Show solution (0 XP)
import unittest, io, re, sys

class TestMath(unittest.TestCase):
    def test_add(self):
        self.assertEqual(1 + 1, 2)

if __name__ == '__main__':
    out = io.StringIO()
    unittest.main(exit=False, testRunner=unittest.TextTestRunner(stream=out, verbosity=0))
    print(re.sub(r'in \d+\.\d+s', 'in 0.000s', out.getvalue().strip()))

Challenge 3 +20 XP

BugHunt: The following code tries to serialize an Expense dataclass directly to JSON, but it crashes. Fix it by converting the dataclass to a dictionary using asdict().

python
  • Test 1 โ€” expects "{\"amount\": 15.5, \"category\": \"Food\"}"
Need a hint? (โˆ’25% XP)

Use the asdict() function from the dataclasses module.

Show solution (0 XP)
import json
from dataclasses import dataclass, asdict

@dataclass
class Expense:
    amount: float
    category: str

e = Expense(15.50, "Food")
print(json.dumps(asdict(e)))