Project Goals
3 min- Fetch 10 multiple-choice questions from Open Trivia DB.
- Shuffle the answers so the correct one isn't always last.
- Decode HTML entities — the API returns
"instead of". - Track score and save the result to
highscores.json.
Warm-Up · Inspect the Endpoint
5 minTry this URL in your browser: https://opentdb.com/api.php?amount=2&type=multiple. You'll see JSON like this:
{
"response_code": 0,
"results": [
{
"type": "multiple",
"difficulty": "easy",
"category": "Science: Computers",
"question": "What does "HTTP" stand for?",
"correct_answer": "HyperText Transfer Protocol",
"incorrect_answers": [
"Hyper Trade Transit Process",
"Hyper Tunnel Transmission Protocol",
"HTML Transit Tunnel Process"
]
}
]
}Two things to handle:
- The HTML entity
"— needs decoding to". - The correct answer is in its own field; you'll merge + shuffle.
Real APIs return real data — with quirks. Decoding entities, shuffling answers and persisting high scores together produce something that feels like a polished product.
Plan · Pipeline
14 minStep 1 — Fetch
import requests r = requests.get( "https://opentdb.com/api.php", params={"amount": 10, "type": "multiple"}, timeout=10, ) r.raise_for_status() data = r.json() questions = data["results"] print(len(questions), "questions fetched")
Step 2 — Decode entities
Use the stdlib html module:
import html print(html.unescape("What does "HTTP" mean?")) # → What does "HTTP" mean?
Step 3 — Shuffle answers
import random answers = q["incorrect_answers"] + [q["correct_answer"]] random.shuffle(answers)
Step 4 — Ask & score
for letter, ans in zip("ABCD", answers): print(f" {letter}) {html.unescape(ans)}") choice = input("answer: ").strip().upper() chosen = answers[ord(choice) - ord("A")] if choice in "ABCD" else None if chosen == q["correct_answer"]: score += 1
Step 5 — Save high score
import json from pathlib import Path FILE = Path("highscores.json") scores = json.loads(FILE.read_text()) if FILE.exists() else [] scores.append({"name": name, "score": score, "out_of": len(questions)}) scores.sort(key=lambda s: s["score"], reverse=True) FILE.write_text(json.dumps(scores[:10], indent=2))
Build · trivia.py
12 min# trivia.py — full quiz from Open Trivia DB import html, json, random, requests from pathlib import Path SCORE_FILE = Path("highscores.json") N = 10 def fetch_questions(amount=N): r = requests.get( "https://opentdb.com/api.php", params={"amount": amount, "type": "multiple"}, timeout=10, ) r.raise_for_status() return r.json()["results"] def ask(q): print(f"\n[{q['category']} · {q['difficulty']}]") print(html.unescape(q["question"])) answers = q["incorrect_answers"] + [q["correct_answer"]] random.shuffle(answers) for letter, ans in zip("ABCD", answers): print(f" {letter}) {html.unescape(ans)}") choice = input("> ").strip().upper() if choice not in "ABCD": return False chosen = answers[ord(choice) - ord("A")] return chosen == q["correct_answer"] def save_score(name, score, total): scores = json.loads(SCORE_FILE.read_text()) if SCORE_FILE.exists() else [] scores.append({"name": name, "score": score, "out_of": total}) scores.sort(key=lambda s: s["score"], reverse=True) SCORE_FILE.write_text(json.dumps(scores[:10], indent=2)) return scores[:5] def main(): name = input("Your name: ").strip() or "Anon" print(f"\n🎯 fetching {N} questions...") qs = fetch_questions(N) score = sum(ask(q) for q in qs) print(f"\n=== {name}: {score}/{N} ===") top5 = save_score(name, score, N) print("\n🏆 leaderboard (top 5)") for i, s in enumerate(top5, 1): print(f" {i}. {s['name']:<10} {s['score']}/{s['out_of']}") if __name__ == "__main__": main()
Sample run
Your name: Aisyah 🎯 fetching 10 questions... [Science: Computers · easy] What does "HTTP" stand for? A) Hyper Tunnel Transmission Protocol B) HTML Transit Tunnel Process C) HyperText Transfer Protocol D) Hyper Trade Transit Process > C ... (9 more) ... === Aisyah: 7/10 === 🏆 leaderboard (top 5) 1. Aisyah 7/10 2. Mei 6/10 ...
sum(ask(q) for q in qs)?Booleans are 1 or 0 in Python. Summing them counts the True ones. A neat, Pythonic alternative to score = 0; if correct: score += 1.
Extensions
13 minAdd a prompt "easy/medium/hard?" and pass it as difficulty= to the API.
Open Trivia DB's categories list is at https://opentdb.com/api_category.php. Show them as a numbered menu, let the user pick, pass category= to the questions endpoint.
Give the player 15 seconds per question. Use time.time() before and after input(); if more than 15s elapsed, count it wrong and print ⏱ time up.
Stretch · Round-Based Difficulty Ramp
8 minBuild a 3-round mode: round 1 is 5 easy questions, round 2 is 5 medium, round 3 is 5 hard. Scoring is weighted — easy = 1 point, medium = 2, hard = 3. Print round totals and a grand total.
Show one possible solution
# trivia_3rounds.py ROUNDS = [("easy", 1), ("medium", 2), ("hard", 3)] total = 0 for diff, weight in ROUNDS: print(f"\n— Round {diff} (×{weight}) —") qs = fetch_questions(5) # (or pass difficulty in the params; depends on API support) correct = sum(ask(q) for q in qs) print(f" → {correct}/5 ×{weight} = {correct * weight}") total += correct * weight print(f"\n🏆 grand total: {total} (out of {sum(5*w for _,w in ROUNDS)})")
Recap
3 minYou combined three skills: API fetch, in-memory transform (shuffle + decode), and persistent storage (JSON leaderboard). That's the shape of dozens of real apps. Notice you didn't write a single new Python feature — you stitched together requests, html, random, json and pathlib into something polished.
Homework
4 minPolish your trivia.py with one extension and one bug-protection upgrade:
- Extension: any one of the three from Section 5.
- Protection: if the API returns
response_code != 0(e.g., not enough questions in that category) print a friendly error and exit gracefully. - Bonus: write a one-paragraph README explaining how to run the program — your future self will thank you.
Sample · response_code guard
def fetch_questions(amount=N, difficulty=None, category=None): params = {"amount": amount, "type": "multiple"} if difficulty: params["difficulty"] = difficulty if category: params["category"] = category r = requests.get("https://opentdb.com/api.php", params=params, timeout=10) r.raise_for_status() data = r.json() if data["response_code"] != 0: raise SystemExit( f"opentdb error code {data['response_code']} — " "try a different category or fewer questions" ) return data["results"]
Non-negotiables: graceful exit, no Python traceback for the user.