Files, Parsing and Validation
What this concept is
Persistence saves data beyond a single run. JSON represents simple values such as lists, numbers and objects as text. Parsing text is different from checking whether its values satisfy your application's rules.
Why it matters here
A tracker must distinguish a first run from a damaged history. Treating every read error as an empty list can silently destroy earlier data.
Concrete example
import json
from pathlib import Path
path = Path("practice-sessions.json")
sessions = [25, 15]
path.write_text(json.dumps(sessions), encoding="utf-8")
loaded = json.loads(path.read_text(encoding="utf-8"))
assert loaded == sessions
import makes library functionality available. Path represents a filesystem path. dumps converts data to JSON text; loads parses JSON text. A relative filename is resolved from the current working directory, not automatically beside the source file. Run this only with practice data because writing replaces existing content.
Split the boundary into distinct questions
A file on disk contains bytes; reading with UTF-8 decodes them into text. Parsing JSON interprets the text as Python values. Application validation decides whether those values belong in the tracker. Saving goes in the reverse direction: validated values, JSON text, encoded file. Successful parsing alone does not complete validation.
For example, [25, 15] in JSON becomes a Python list of integers. {"minutes": 25} becomes a dictionary: valid JSON, wrong outer shape. [true] becomes [True]: valid JSON, wrong element type. [241] has the right shape and type, but a value outside our allowed range.
Learn text operations before writing a parser
raw = " 25, 15 "
fields = raw.split(",")
assert fields == [" 25", " 15 "]
cleaned = fields[0].strip()
assert cleaned == "25"
assert int(cleaned) == 25
assert "10,".split(",") == ["10", ""]
A method, such as raw.split, is an operation reached through an object with a dot. Parentheses call it. split separates fields; strip removes surrounding whitespace. These operations return new values rather than changing the original string. The final assertion is important: a trailing comma supplies an empty field. Do not discard it merely to make conversion succeed.
Report one conversion error without hiding other failures
raw = "two"
try:
minutes = int(raw)
except ValueError:
print("Enter a whole number of minutes")
else:
print(f"Converted: {minutes}")
The try block contains the operation that may fail. A matching except handles that failure. The else runs only when conversion succeeds. Here the message prints; the success block does not run. Keep the protected operation small so you know which failure you handled. Later, the loader handles a missing file as first-run history but reports malformed data. Catching everything and returning an empty history would erase this distinction.
Keep practice files and stored history understandable
Path.cwd() shows the process's current working folder. path.resolve() shows the absolute location of the relative path used by the example. A filename ending in .json is a convention, not proof of valid contents. Opening the file in a text editor is a useful way to inspect what was actually saved; do not edit your only copy of important data for an experiment.
For this lab, follow load → parse new input → combine → calculate → save → report success. If either loading or parsing fails, do not save. A rejected input should leave the existing file exactly as it was. This order protects against those validation failures, but write_text can still truncate a file before a later write failure. It does not make a save atomic or safe against power loss or two writers. Record that limitation rather than claiming a production storage guarantee.
Common confusion / misconception
Valid JSON is not necessarily valid tracker data: {}, [true] and [0] parse but violate our contract. In Python, a Boolean is a kind of integer for some checks; use type(value) is int when the contract explicitly excludes it. Missing files can mean first run; malformed JSON must be reported.
How to use it
Load existing data before writing. Catch FileNotFoundError to return an empty history. Let malformed JSON raise ValueError (its parser error is a subclass). Check the outer list and every element. Parse new input before saving so rejected input cannot erase history.
Check yourself
Should "10," mean one session? Under our contract no: the second field is empty. Should a broken file be overwritten with []? No: report the error and preserve it.
Mini drill or application
Save and reload a practice list. Replace the JSON with invalid text and confirm your loader raises without writing. Explain why this single-process exercise still does not guarantee crash-safe writes.
Read this only if stuck
Official source: Python tutorial. Read only the matching section after attempting the example. These lessons and exercises are original curriculum material.