Project Goals
3 minBy the end of this project you can:
- Design a JSON schema for quiz questions.
- Validate the loaded JSON before running the quiz.
- Score the player, then save the score in a separate JSON file.
- Show all-time stats by reading the score history.
The Question File
5 minCreate quiz.json next to your Python file. The schema:
{ "title": "Python Basics", "questions": [ { "q": "What does print() do?", "options": ["Eats keyboards", "Shows text on screen", "Prints to paper"], "answer": 1 }, { "q": "What is 2 ** 3?", "options": ["6", "8", "9"], "answer": 1 }, { "q": "Which keyword starts a function definition?", "options": ["fn", "function", "def"], "answer": 2 }, { "q": "What does len('hello') return?", "options": ["4", "5", "6"], "answer": 1 } ] }
Each question has a q, a list of options, and an answer index (0-based). Add at least 5 questions of your own for the homework — anything you fancy.
Task 1 · Load & Validate
10 minLoad the file, then check it's shaped how the engine expects. Fail loudly if not.
import json def load_quiz(path): with open(path, encoding="utf-8") as f: data = json.load(f) # Validate if "questions" not in data or not isinstance(data["questions"], list): raise ValueError("Missing or invalid 'questions' list") for i, q in enumerate(data["questions"]): for key in ("q", "options", "answer"): if key not in q: raise ValueError(f"Question {i + 1} missing '{key}'") if not isinstance(q["options"], list) or len(q["options"]) < 2: raise ValueError(f"Question {i + 1} needs at least 2 options") if not isinstance(q["answer"], int): raise ValueError(f"Question {i + 1} answer must be an int") if not (0 <= q["answer"] < len(q["options"])): raise ValueError(f"Question {i + 1} answer index out of range") return data
Loading is one line; validating is the rest. Real apps validate every external input — assume the file is wrong until it proves otherwise.
Task 2 · Run the Quiz
10 minimport random def run_quiz(quiz): print(f"\n=== {quiz['title']} ===") questions = quiz["questions"][:] random.shuffle(questions) # different order each run score = 0 for i, q in enumerate(questions, start=1): print(f"\nQ{i}: {q['q']}") for j, opt in enumerate(q["options"]): print(f" {j + 1}. {opt}") while True: try: pick = int(input("Your answer (number): ")) - 1 if 0 <= pick < len(q["options"]): break except ValueError: pass print(" ! Pick a valid number.") if pick == q["answer"]: print(" ✓ Correct!") score += 1 else: correct = q["options"][q["answer"]] print(f" ✗ Wrong. Answer was: {correct}") print(f"\nFinal score: {score} / {len(questions)}") return score, len(questions)
One pure function. It takes a quiz dict, returns (score, total). Easy to test, easy to wrap.
Task 3 · Save the Score
8 minAfter each quiz, append a record to scores.json.
from datetime import datetime def save_score(quiz_title, name, score, total): try: with open("scores.json", encoding="utf-8") as f: history = json.load(f) except FileNotFoundError: history = [] history.append({ "quiz": quiz_title, "name": name, "score": score, "total": total, "when": datetime.now().isoformat(timespec="seconds"), }) with open("scores.json", "w", encoding="utf-8") as f: json.dump(history, f, indent=2)
Task 4 · Show All-Time Stats
5 mindef show_stats(): try: with open("scores.json", encoding="utf-8") as f: history = json.load(f) except FileNotFoundError: print("(no history yet)") return print(f"\n=== All-Time Scores ({len(history)} entries) ===") for h in history[-10:]: pct = round(h["score"] / h["total"] * 100) print(f" {h['when']} {h['name']:<12} {h['quiz']:<24} " f"{h['score']}/{h['total']} ({pct}%)")
Putting It All Together · quiz.py
8 minAssemble the four tasks into one menu-driven app.
Show one complete solution
# quiz.py — JSON-backed quiz engine import json, random from datetime import datetime def load_quiz(path): with open(path, encoding="utf-8") as f: data = json.load(f) if "questions" not in data: raise ValueError("Missing 'questions'") for i, q in enumerate(data["questions"]): for k in ("q", "options", "answer"): if k not in q: raise ValueError(f"Q{i + 1} missing '{k}'") if not (0 <= q["answer"] < len(q["options"])): raise ValueError(f"Q{i + 1} answer index out of range") return data def run_quiz(quiz): print(f"\n=== {quiz['title']} ===") questions = quiz["questions"][:] random.shuffle(questions) score = 0 for i, q in enumerate(questions, start=1): print(f"\nQ{i}: {q['q']}") for j, opt in enumerate(q["options"]): print(f" {j + 1}. {opt}") while True: try: pick = int(input("Your answer: ")) - 1 if 0 <= pick < len(q["options"]): break except ValueError: pass print(" ! Invalid choice.") if pick == q["answer"]: print(" ✓ Correct!") score += 1 else: print(f" ✗ Wrong. Answer: {q['options'][q['answer']]}") print(f"\nFinal score: {score} / {len(questions)}") return score, len(questions) def save_score(title, name, score, total): try: with open("scores.json", encoding="utf-8") as f: history = json.load(f) except FileNotFoundError: history = [] history.append({"quiz": title, "name": name, "score": score, "total": total, "when": datetime.now().isoformat(timespec="seconds")}) with open("scores.json", "w", encoding="utf-8") as f: json.dump(history, f, indent=2) def show_stats(): try: with open("scores.json", encoding="utf-8") as f: history = json.load(f) except FileNotFoundError: print("(no history yet)") return print(f"\n=== Last 10 of {len(history)} ===") for h in history[-10:]: pct = round(h["score"] / h["total"] * 100) print(f" {h['when']} {h['name']:<10} {h['quiz']:<20} {h['score']}/{h['total']} ({pct}%)") # Main menu try: quiz = load_quiz("quiz.json") except FileNotFoundError: print("quiz.json not found.") raise SystemExit except (json.JSONDecodeError, ValueError) as e: print(f"Bad quiz file: {e}") raise SystemExit name = input("Your name: ").strip() or "anon" while True: print("\n1 play 2 stats 3 quit") pick = input("Choose: ") if pick == "1": score, total = run_quiz(quiz) save_score(quiz["title"], name, score, total) elif pick == "2": show_stats() elif pick == "3": print("Bye!") break
Non-negotiables: validated load, randomised question order, save + show stats. Try swapping quiz.json for a different topic — the engine doesn't care.
Recap
3 minJSON turns "configuration" from code into data. The quiz engine is one file; the quiz content is another. Schools, libraries and tutoring apps work exactly this way — one engine, many quiz files. Validation catches typos in the file before they corrupt the user's experience. Scores persist across runs. The pattern scales: same shape works for a flashcard app, a vocabulary trainer, a study deck.
Two lessons left in Level 2. Tomorrow's Code Olympics tests every skill from L2-01 to L2-46. The day after is the capstone project — bringing JSON, regex, files, dicts and functions into one polished CLI app.
Homework
4 minWrite your own quiz.json with at least 10 questions on a topic you love — anime, football, K-pop, geography, anything. Then upgrade your quiz.py with:
- A difficulty field per question (
"easy","hard"). At the menu, let the user pick a difficulty; the engine filters questions to that level. - A category field per question. Print the category at the end alongside the score breakdown by category.
Sample · category breakdown
# After the quiz: by_category = {} # {category: [correct, total]} for q in questions: cat = q.get("category", "general") by_category.setdefault(cat, [0, 0]) by_category[cat][1] += 1 if pick == q["answer"]: by_category[cat][0] += 1 print("\n=== By category ===") for cat, (correct, total) in by_category.items(): print(f" {cat:<15} {correct}/{total}")
Non-negotiables: at least 10 questions in your JSON, a difficulty filter that uses comprehension, and a per-category breakdown. dict.setdefault(key, default) is a slick way to "ensure key exists, then proceed" — common Pythonic idiom.