Learn
05.2 | CSV FILES AND JSON DATA
01 | STRUCTURE NEEDS MORE THAN A TEXT LINE
Suppose an expense has a date, category, amount and note. A single line of text could hold them, but how does a later program know where each field ends? A note might itself contain a comma. A standard format provides rules for boundaries and escaping.
CSV suits tabular records with columns. JSON suits nested combinations of lists and objects. Both are text formats, so you can inspect them in an editor, but neither automatically establishes that an expense is true or valid.
Choose based on the intended use. The project uses JSON for its saved record list and CSV for a readable export. It does not import that export back into the tracker.
02 | CSV USES ROWS AND FIELDS
CSV stands for comma-separated values, although variants may use other delimiters. A row has fields in column order. A header row can label those columns. When a field contains a delimiter, quotation marks or line break, quoting rules preserve it as one field.
Do not parse general CSV with line.split(","). The line date,food,2.50,"Lunch, drink" includes a comma inside one note. A CSV parser understands the quotes and gives four fields rather than five.
Python’s csv module reads and writes these rules. Open CSV files with newline="" so the module can manage line endings correctly, including newline characters within quoted fields.
import csv
with open("expenses.csv", "w", encoding="utf-8", newline="") as file:
writer = csv.writer(file)
writer.writerow(["date", "category", "amount", "note"])
writer.writerow(["2026-10-01", "food", "2.50", "Lunch, drink"])
with open("expenses.csv", "r", encoding="utf-8", newline="") as file:
for row in csv.reader(file):
print(row)EXPECTED OUTPUT
['date', 'category', 'amount', 'note'] ['2026-10-01', 'food', '2.50', 'Lunch, drink']
03 | CSV FIELDS ARE USUALLY TEXT
The ordinary CSV reader returns strings for fields. It does not infer that 2.50 means a currency amount or that a date is a real calendar day. Decide conversions and validation after parsing.
DictReader uses the header values as dictionary keys. That makes row["amount"] clearer than guessing a column index, but the header must still match the expected schema. Duplicate names, missing columns and unexpected fields need explicit checks in a robust importer.
DictWriter accepts dictionaries and a fieldnames sequence defining output columns. writeheader() creates the labels. Normal rows can then be written without manually escaping every note.
import csv
with open("people.csv", "w", encoding="utf-8", newline="") as file:
writer = csv.DictWriter(file, fieldnames=["name", "score"])
writer.writeheader()
writer.writerow({"name": "Maya", "score": 8})
with open("people.csv", encoding="utf-8", newline="") as file:
row = next(csv.DictReader(file))
print(row["name"], repr(row["score"]))EXPECTED OUTPUT
Maya '8'
04 | JSON STORES NESTED DATA
JSON means JavaScript Object Notation, but Python and many other languages use it. A JSON object resembles a Python dictionary with string keys; an array resembles a list. JSON has strings, numbers, true, false and null.
In Python, the json module maps ordinary JSON objects to dictionaries, arrays to lists, true and false to Boolean values, and null to None. JSON source uses double quotes for strings and its own literal spellings. It is not a Python expression.
Do not use eval() to load a data file. json.load parses JSON data rather than executing arbitrary Python instructions. Parsing is still separate from checking the expected fields, types and permitted values.
import json
text = '{"name": "Maya", "scores": [8, 10], "finished": true, "comment": null}'
record = json.loads(text)
print(record["name"])
print(record["scores"][1])
print(record["finished"], record["comment"])EXPECTED OUTPUT
Maya 10 True None
05 | LOAD AND DUMP USE FILES
json.dump writes a Python value to an open text file as JSON. json.load reads one JSON document from a file. The related dumps and loads functions work with strings: the final s can remind you that these versions produce or consume a string.
indent=2 formats the document for human inspection. ensure_ascii=False allows characters such as é to appear directly when the file encoding supports them. Neither option changes the application meaning of the record.
Write one complete document. Appending a second JSON list after the first does not extend the existing list; it creates multiple adjacent documents that ordinary json.load rejects.
import json
records = [{"name": "Maya", "scores": [8, 10]}]
with open("records.json", "w", encoding="utf-8") as file:
json.dump(records, file, ensure_ascii=False, indent=2)
with open("records.json", "r", encoding="utf-8") as file:
restored = json.load(file)
print(restored == records)EXPECTED OUTPUT
True
06 | KNOW WHAT A ROUND TRIP PRESERVES
A round trip writes data and reads it back. Ordinary lists, string-keyed dictionaries and JSON-compatible values can preserve the intended structure. A tuple becomes a JSON array and reads back as a list. Sets do not directly serialize with the default encoder.
JSON object keys are strings. Avoid assuming integer-keyed dictionaries return with the same key types. Dates need an agreed representation such as an ISO-formatted string.
JSON syntax alone does not enforce a record schema. {"amount_cents": "250"} is valid JSON but stores text instead of the integer the tracker needs. Validate after decoding before using it in calculations.
07 | MONEY NEEDS A REPRESENTATION RULE
Binary floating-point values are useful for many measurements, but decimal money arithmetic can expose representation rounding. Formatting to two decimal places only changes displayed text; it does not make the stored float exact.
The project uses whole-number cents in one currency. CHF 2.50 becomes integer 250. Adding 10 and 20 cents gives exactly 30 cents under integer arithmetic. The input rule accepts a positive amount with at most two decimal places; it does not silently round a third decimal.
A mature financial application may use Decimal and additional accounting rules. Here, integer cents keeps one clear learning contract. Mixing currencies without conversion would still make a total meaningless, so the tracker explicitly uses CHF only.
amounts_cents = [10, 20, 250]
total = sum(amounts_cents)
print(f"CHF {total // 100}.{total % 100:02d}")EXPECTED OUTPUT
CHF 2.80
08 | INSPECT A JSON RECORD
Edit the JSON below and ask the model to decode it. Then it separately checks a small demonstration schema. Successful syntax decoding does not automatically mean the shape or values fit that schema.
09 | FAIL CLEARLY ON DAMAGED DATA
Malformed JSON raises JSONDecodeError. An unreadable file can raise OSError or a decoding exception. A syntactically valid but unsuitable record needs a separate validation error. Keep these different failures visible enough to diagnose.
For the project, a missing save file starts a new list. An existing corrupted file stops startup and stays available for inspection. Treating every failure as [] risks replacing evidence of earlier expenses with an empty dataset.
JSON and CSV are formats, not databases with automatic transactions, access controls or multi-user coordination. The project is a small local program for one process, which keeps the scope manageable.
10 | TEST THE FORMAT WITH AWKWARD TEXT
Use a note containing a comma, quotation marks and a Unicode character. Save and reload it using the proper module. Check exact field values, not merely whether the output looks plausible.
For JSON, test a list of records, an empty list, malformed text and valid text with the wrong root type. For CSV, verify the header and that a quoted comma remains inside one note.
A file extension is a label, not proof of its contents. A file called expenses.json may contain invalid JSON. Trust the parser and your schema checks rather than the name alone.
PUT YOUR SKILLS TO WORK
Use the Section 5 challenges to practise these ideas independently. Work in your Python editor, predict results and keep a short test record.
Section 5 challenges Python course navigation