Turn transactions into a spending explanation
A useful tracker does more than add numbers. It records when money was spent, assigns a consistent category, preserves a description, survives program restarts and answers questions such as “Which category consumes the most?” and “Am I over my September budget?”
Capture
Validate date, category, description and positive amount.
Persist
Append genuine records to a reusable CSV file.
Explain
Sort, aggregate, calculate shares and check budgets.
Protect the data at entry and loading
| Field | Rule | Representation |
|---|---|---|
| Date | Valid ISO date YYYY-MM-DD | datetime.date |
| Category | Non-empty; normalized title case | str |
| Description | Non-empty | str |
| Amount | Positive; two decimal places | Decimal |
Every in-memory Expense is valid. Invalid user input is never appended, and malformed existing rows are reported with line numbers and skipped rather than crashing the entire report.
An immutable dataclass represents one transaction
Expense(spent_on: date, category: str,
description: str, amount: Decimal)@dataclass(frozen=True) generates readable initialization and comparison while preventing accidental field reassignment. Parsing functions convert external strings into domain types before an object is created.
Why Decimal?
Binary floating point cannot represent many base-ten currency fractions exactly. Decimal("19.90") preserves decimal intent and explicit two-place quantization. Production systems also need a documented rounding rule and currency identifier.
Why separate parsing?
parse_date and parse_amount create one validation path shared by keyboard input and CSV loading. Duplicated validation eventually produces inconsistent behavior.
The file is data—not simulated standard input
date,category,description,amount 2026-09-01,Food,Lunch,120.00 2026-09-02,Travel,Bus pass,850.00
The program opens expenses.csv with UTF-8 and newline="", lets Python’s CSV module quote commas safely and writes the header only for a new or empty file. Appending one record is O(1) file growth.
CSV is human-readable and portable, but it has no schema enforcement, transaction support or safe multi-user concurrency. SQLite is a natural next step when editing, deletion, concurrent access or richer queries become important.
Aggregate with a dictionary, then sort by meaning
- Create
defaultdict(Decimal)keyed by category. - For each expense, add its amount to exactly one total.
- Sum category totals to obtain the grand total.
- Calculate category share:
amount×100/grand_total. - Sort by descending amount, then category name for deterministic ties.
Monthly budget
spent = sum(amount where date formatted as YYYY-MM equals selected month) remaining = budget − spent
A negative remaining value is displayed as a positive “over budget” amount. This presentation avoids asking the user to interpret a double negative.
Runnable Python program
Loading source…For real persistence, run locally in a writable folder. Many online compilers use temporary storage, so downloaded CSV data may disappear after the session.
Trace one addition and summary
- Validate the date.
- Normalize text.
- Parse money exactly.
- Build the domain object.
- Prepare persistence.
- Save the record.
- Read for reporting.
- Aggregate by category.
- Produce deterministic summary.
Press Next to begin.
Test calculations and file behavior
Valid append and reload
Currency precision
Invalid fields
Malformed stored row
Budget boundaries
Reports scan the file once
| Operation | Time | Memory |
|---|---|---|
| Append expense | O(1) | O(1) |
| Load n rows | O(n) | O(n) |
| Category summary | O(n+c log c) | O(c) |
| List sorted by date | O(n log n) | O(n) |
Here c is the number of categories. The simple design reloads for every menu action so the file is the source of truth. A larger application would cache data carefully or query a database.
Check the design
Why is Decimal preferred for stored money?
Why use csv.writer instead of joining fields with commas?
Extensions
- Add transaction IDs and safe edit/delete operations.
- Generate monthly trend reports and category limits.
- Move persistence to SQLite with parameterized queries.
- Add import/export with duplicate detection.
- Write automated tests using temporary directories.
Explain reliability choices
Why use a dataclass?
It expresses the record schema clearly and removes repetitive initialization code while retaining normal Python objects.
How are malformed rows handled?
Each row is parsed independently inside a specific exception boundary, so one bad row is reported without losing valid records.
What is CSV’s main limitation here?
It lacks transactions, indexes and concurrency control, making update/delete and multiple writers fragile.
How would you make appending crash-safe?
For stronger guarantees, write a complete validated replacement to a temporary file, flush as required, then atomically replace the original—or use a transactional database.
Useful automation begins with trustworthy data
Collections and reports matter only after external strings become valid domain values. This tracker combines typed modeling, Decimal arithmetic, genuine persistence, defensive loading and auditable totals.
