A terminal quiz is an ideal beginner project because it transforms basic Python concepts into a complete interactive program. Questions are stored as data, the user selects answers, the program validates input, points are calculated, and a final result is displayed.
This tutorial builds a complete terminal quiz game in Python. The final version includes multiple-choice questions, shuffled options, input validation, explanations, scoring, replay, optional JSON loading, and small functions that are easy to test.
The project relies on lists and dictionaries. Review the Python lists guide and Python data types guide when those structures are new to you.
Plan the Quiz Data
Each question needs a prompt, a collection of options, the correct answer, and optionally an explanation. A dictionary represents one question, and a list stores the complete quiz.
QUESTIONS = [
{
"prompt": "Which keyword defines a function in Python?",
"options": ["class", "def", "return", "import"],
"answer": "def",
"explanation": "Python functions begin with the def keyword.",
},
{
"prompt": "Which type stores unique values?",
"options": ["list", "tuple", "set", "str"],
"answer": "set",
"explanation": "A set keeps unique hashable elements.",
},
]Storing the answer as the actual text makes option shuffling easier. An answer letter such as B would become incorrect after the options change order.
Display One Question
Use enumerate() to create numbered choices:
def display_question(question, number, total):
print()
print(f"Question {number} of {total}")
print(question["prompt"])
for option_number, option in enumerate(
question["options"],
start=1,
):
print(f" {option_number}. {option}")The detailed Python enumerate guide explains how the custom starting value works.
Read and Validate an Answer
The built-in input() function returns text. The official Python input documentation describes its behavior.
def read_option(number_of_options):
while True:
raw_answer = input("Your answer: ").strip()
try:
selected = int(raw_answer)
except ValueError:
print("Enter the number of an option.")
continue
if 1 <= selected <= number_of_options:
return selected - 1
print(f"Choose a number from 1 to {number_of_options}.")The function returns a zero-based list index. Invalid input does not end the program and does not count as an answered question.
Check the Answer
def is_correct(question, selected_index):
selected_text = question["options"][selected_index]
return selected_text == question["answer"]This pure function is easy to test because it does not read input or print output.
Shuffle Questions and Options
Use a dedicated random generator so a fixed seed can reproduce a quiz during tests.
import random
def prepare_questions(questions, generator=None):
generator = generator or random.Random()
prepared = []
for question in questions:
copied_question = {
**question,
"options": list(question["options"]),
}
generator.shuffle(copied_question["options"])
prepared.append(copied_question)
generator.shuffle(prepared)
return preparedCopying prevents the original data from being permanently reordered. The Python random module guide covers shuffling and deterministic seeds.
Calculate a Percentage
def calculate_percentage(score, total):
if total == 0:
return 0.0
return score / total * 100Handling an empty quiz avoids division by zero. The function uses a clear return value rather than modifying a global variable.
Complete Terminal Quiz Game
import random
QUESTIONS = [
{
"prompt": "Which keyword defines a function in Python?",
"options": ["class", "def", "return", "import"],
"answer": "def",
"explanation": "Python functions begin with the def keyword.",
},
{
"prompt": "Which type stores unique values?",
"options": ["list", "tuple", "set", "str"],
"answer": "set",
"explanation": "A set keeps unique hashable elements.",
},
{
"prompt": "What does len([10, 20, 30]) return?",
"options": ["2", "3", "30", "60"],
"answer": "3",
"explanation": "len() returns the number of list elements.",
},
{
"prompt": "Which statement handles an exception?",
"options": ["if/else", "try/except", "for/in", "match/case"],
"answer": "try/except",
"explanation": "try/except handles exceptions raised by code.",
},
{
"prompt": "Which file extension normally stores Python code?",
"options": [".js", ".html", ".py", ".csv"],
"answer": ".py",
"explanation": "Python source files normally use the .py extension.",
},
]
def prepare_questions(questions, generator=None):
generator = generator or random.Random()
prepared = []
for question in questions:
copied_question = {
**question,
"options": list(question["options"]),
}
generator.shuffle(copied_question["options"])
prepared.append(copied_question)
generator.shuffle(prepared)
return prepared
def display_question(question, number, total):
print()
print(f"Question {number} of {total}")
print(question["prompt"])
for option_number, option in enumerate(
question["options"],
start=1,
):
print(f" {option_number}. {option}")
def read_option(number_of_options):
while True:
raw_answer = input("Your answer: ").strip()
try:
selected = int(raw_answer)
except ValueError:
print("Enter the number of an option.")
continue
if 1 <= selected <= number_of_options:
return selected - 1
print(f"Choose a number from 1 to {number_of_options}.")
def is_correct(question, selected_index):
selected_text = question["options"][selected_index]
return selected_text == question["answer"]
def calculate_percentage(score, total):
if total == 0:
return 0.0
return score / total * 100
def play_quiz(questions, generator=None):
prepared = prepare_questions(questions, generator)
score = 0
for number, question in enumerate(prepared, start=1):
display_question(question, number, len(prepared))
selected_index = read_option(len(question["options"]))
if is_correct(question, selected_index):
score += 1
print("Correct!")
else:
selected = question["options"][selected_index]
print(f"Not quite. You chose: {selected}")
print(f"Correct answer: {question['answer']}")
explanation = question.get("explanation")
if explanation:
print(explanation)
percentage = calculate_percentage(score, len(prepared))
print()
print("Quiz complete")
print(f"Score: {score}/{len(prepared)}")
print(f"Percentage: {percentage:.1f}%")
if percentage == 100:
print("Perfect score!")
elif percentage >= 70:
print("Great work!")
elif percentage >= 50:
print("Good start. Review the explanations and try again.")
else:
print("Keep practicing and play another round.")
return score
def wants_to_play_again():
while True:
print()
answer = input("Play again? [y/n]: ").strip().lower()
if answer in {"y", "yes"}:
return True
if answer in {"n", "no"}:
return False
print("Enter y or n.")
def main():
print("Python Terminal Quiz")
while True:
play_quiz(QUESTIONS)
if not wants_to_play_again():
print("Thanks for playing!")
break
if __name__ == "__main__":
main()Save the file as quiz.py and run:
python quiz.pyLoad Questions from JSON
Keeping questions outside the source code lets a teacher or editor update the quiz without changing its logic. Create questions.json:
[
{
"prompt": "Which keyword defines a function in Python?",
"options": ["class", "def", "return", "import"],
"answer": "def",
"explanation": "Python functions begin with def."
}
]Load it with the standard-library json module:
import json
from pathlib import Path
def load_questions(path):
path = Path(path)
with path.open("r", encoding="utf-8") as file:
questions = json.load(file)
if not isinstance(questions, list):
raise ValueError("The JSON root must be a list.")
return questionsThe official Python JSON documentation explains encoding and serialization behavior. The pathlib guide covers reliable file paths.
Validate External Question Data
JSON can be valid while still missing required quiz fields. Validate each question before starting:
def validate_questions(questions):
required = {"prompt", "options", "answer"}
for number, question in enumerate(questions, start=1):
missing = required - question.keys()
if missing:
raise ValueError(
f"Question {number} is missing: {sorted(missing)}"
)
if question["answer"] not in question["options"]:
raise ValueError(
f"Question {number} answer is not in its options"
)Failing early with a clear message is better than producing a mysterious error halfway through a quiz.
Add Command-Line Options
A larger version can accept a question file, question limit, or random seed:
import argparse
def build_parser():
parser = argparse.ArgumentParser(
description="Run a terminal quiz."
)
parser.add_argument("--file", default="questions.json")
parser.add_argument("--limit", type=int)
parser.add_argument("--seed", type=int)
return parserThe argparse CLI guide explains validation, flags, and subcommands.
Test the Quiz Logic
Functions that do not call input() can be tested directly:
from quiz import calculate_percentage, is_correct
def test_calculate_percentage():
assert calculate_percentage(4, 5) == 80.0
assert calculate_percentage(0, 0) == 0.0
def test_is_correct():
question = {
"options": ["list", "set"],
"answer": "set",
}
assert is_correct(question, 1) is True
assert is_correct(question, 0) is FalseThe Python unit testing guide shows how to mock input and capture output when you want to test interactive functions too.
Common Mistakes
- Storing only an answer letter and then shuffling the options.
- Trusting external JSON without validating required fields.
- Converting input to an integer without handling
ValueError. - Using global variables for every part of the score and question state.
- Shuffling the original data and making tests unpredictable.
- Mixing question data, display logic, input, and scoring in one large loop.
- Accepting an answer index outside the available option range.
Ideas to Improve the Game
- Add categories and difficulty levels.
- Select only a requested number of questions.
- Save high scores to JSON or SQLite.
- Add a time limit for each question.
- Support true/false and free-text questions.
- Color terminal output with an optional library.
- Create a graphical version with Tkinter or Pygame.
- Publish question packs separately from the program.
Frequently Asked Questions
Do I need an external package?
No. The complete version uses only the Python standard library.
Why use dictionaries for questions?
A dictionary gives clear names to related values such as the prompt, options, answer, and explanation.
Can the options be letters instead of numbers?
Yes. Generate labels with chr() or a predefined alphabet, then convert the selected letter back to an index.
How do I keep the same random order for a test?
Pass random.Random(seed) to prepare_questions() or play_quiz().
How do I add questions without changing Python code?
Store them in JSON, validate the loaded structure, and pass the resulting list into the game.
Conclusion
A terminal quiz game in Python brings together data structures, loops, validation, functions, randomization, files, and tests in one approachable project. Separating the quiz data from the game logic makes the program easier to expand and maintain.
Begin with three questions and a simple score. Once that version is reliable, add JSON files, categories, command-line options, and saved results one feature at a time.






