Learning overview
- Estimated time
- 4 minutes
- Difficulty
- Beginner
- Prerequisites
- No prior experience required
- Learning outcome
- Explain python using a clear mental model · Apply python in practical Python work
- Last updated
- July 18, 2026
What you will build and learn
The tracker records a date, category, description, and positive amount. It can list records and summarize spending by category. Keep parsing, validation, calculations, and file access separate so each part can be tested.
- Model records with dictionaries
- Validate text, dates, and decimal amounts
- Read and write CSV with context managers
- Test calculations without touching the filesystem
Step 1: Define and validate an expense
Decimal avoids binary floating-point surprises for money. ISO dates sort naturally and are easy to validate with date.fromisoformat.
from datetime import date
from decimal import Decimal, InvalidOperation
def create_expense(date_text: str, category: str, description: str, amount_text: str) -> dict:
spent_on = date.fromisoformat(date_text)
amount = Decimal(amount_text)
if amount <= 0:
raise ValueError("Amount must be positive")
category = category.strip()
if not category:
raise ValueError("Category is required")
return {
"date": spent_on.isoformat(),
"category": category,
"description": description.strip(),
"amount": amount,
}Step 2: Write a pure summary function
The function accepts records and returns new data without printing or reading files. That makes success, empty input, and repeated categories straightforward to test.
from collections import defaultdict
from decimal import Decimal
def totals_by_category(expenses: list[dict]) -> dict[str, Decimal]:
totals: defaultdict[str, Decimal] = defaultdict(Decimal)
for expense in expenses:
totals[expense["category"]] += expense["amount"]
return dict(sorted(totals.items()))Step 3: Append records to CSV
Pathlib represents paths consistently. Open with newline="" as recommended by the csv module and specify UTF-8 explicitly. Write the header only when the file is new or empty.
import csv
from pathlib import Path
FIELDS = ["date", "category", "description", "amount"]
def append_expense(path: Path, expense: dict) -> None:
needs_header = not path.exists() or path.stat().st_size == 0
with path.open("a", encoding="utf-8", newline="") as file:
writer = csv.DictWriter(file, fieldnames=FIELDS)
if needs_header:
writer.writeheader()
writer.writerow({**expense, "amount": str(expense["amount"])})Step 4: Load and parse saved records
Convert amounts at the storage boundary so the rest of the application consistently receives Decimal values. Let malformed rows produce a useful error instead of silently corrupting totals.
import csv
from decimal import Decimal
from pathlib import Path
def load_expenses(path: Path) -> list[dict]:
if not path.exists():
return []
with path.open(encoding="utf-8", newline="") as file:
return [
{**row, "amount": Decimal(row["amount"])}
for row in csv.DictReader(file)
]Step 5: Keep the command interface thin
Use argparse to parse commands, call the focused functions, and translate expected validation errors into concise messages. Avoid placing calculation or CSV details in the command handler.
- add DATE CATEGORY AMOUNT --description TEXT
- list
- summary
- Exit non-zero for invalid commands or data
Step 6: Test and extend safely
Test create_expense and totals_by_category with pytest. Use a temporary path for storage tests. After the core path is stable, add month filtering, budgets, JSON export, or a SQLite repository behind the same function boundaries.
from decimal import Decimal
from expenses import totals_by_category
def test_totals_by_category_combines_matching_categories():
expenses = [
{"category": "Food", "amount": Decimal("4.50")},
{"category": "Food", "amount": Decimal("5.50")},
]
assert totals_by_category(expenses) == {"Food": Decimal("10.00")}Frequently asked questions
Why use Decimal instead of float for expenses?
Decimal represents decimal quantities according to explicit decimal arithmetic, avoiding common binary floating-point representation surprises in financial calculations.
Why separate file access from calculations?
Pure calculations are fast and deterministic to test. Storage can then change from CSV to SQLite without rewriting the business rules.
Should invalid CSV rows be skipped?
Not silently. Report the row and reason, or provide an explicit tolerant import mode, so users know their totals may be incomplete.
Topic graph
A taxonomy-generated path through this subject.
Related courses
Related learning
Recommended automatically from shared technologies, topics, intent, and difficulty.
Related Guides
Related Tutorials
Related Glossary
Related Cheatsheets
Related Projects
Related Interview Questions
Related reading
Hand-picked companion pages that deepen this topic.
Your next steps
