How to Build a Python Quiz App Step by Step: Complete Tutorial with Source Code

Building a quiz app is one of the most rewarding Python projects for beginners because it combines multiple programming concepts into a single, interactive application. By the end of this tutorial, you will have a fully functional quiz game that loads questions from a file, tracks scores, implements a timer, and provides detailed feedback.

This guide shows you exactly how to build a Python quiz app step by step, starting from a simple version and progressively adding features until you have something portfolio-worthy.


What You Will Learn

By completing this project, you will practice:

  • Object-Oriented Programming (OOP): Classes, methods, and encapsulation
  • File handling: Loading quiz data from JSON files
  • Control flow: Loops, conditionals, and user input validation
  • Error handling: Try/except blocks for robust code
  • Standard library modules: json, random, time
  • Code organization: Separating data, logic, and presentation

Prerequisites

Before starting, make sure you have:


Project Structure

We will organize the project into clean, separate files:

python_quiz_app/
    main.py           # Entry point - runs the quiz
    quiz_engine.py    # Core quiz logic (classes)
    questions.json    # Quiz question data
    scores.json       # Persistent score history

Step 1: Design the Question Data Format

First, create a questions.json file that stores your quiz questions. Using JSON keeps your data separate from your code, making it easy to add new questions without modifying any Python files.

{
  "metadata": {
    "title": "Python Fundamentals Quiz",
    "version": "1.0",
    "total_questions": 10
  },
  "questions": [
    {
      "id": 1,
      "question": "What is the output of: print(type(3.14))?",
      "options": ["<class 'int'>", "<class 'float'>", "<class 'str'>", "<class 'decimal'>"],
      "correct_answer": 1,
      "difficulty": "easy",
      "explanation": "3.14 is a floating-point number, so Python assigns it the 'float' type."
    },
    {
      "id": 2,
      "question": "Which keyword is used to define a function in Python?",
      "options": ["function", "func", "def", "define"],
      "correct_answer": 2,
      "difficulty": "easy",
      "explanation": "The 'def' keyword is used to define functions in Python."
    },
    {
      "id": 3,
      "question": "What does the 'len()' function return when called on a dictionary?",
      "options": ["Number of values", "Number of key-value pairs", "Total characters in keys", "Memory size"],
      "correct_answer": 1,
      "difficulty": "easy",
      "explanation": "len() on a dictionary returns the number of key-value pairs it contains."
    },
    {
      "id": 4,
      "question": "Which of the following is immutable in Python?",
      "options": ["list", "dictionary", "set", "tuple"],
      "correct_answer": 3,
      "difficulty": "medium",
      "explanation": "Tuples are immutable - once created, their elements cannot be changed."
    },
    {
      "id": 5,
      "question": "What is the correct way to handle a file that might not exist?",
      "options": ["if file.exists():", "try/except FileNotFoundError", "check file.open()", "use file.safe_read()"],
      "correct_answer": 1,
      "difficulty": "medium",
      "explanation": "try/except FileNotFoundError is the Pythonic way to handle potentially missing files."
    },
    {
      "id": 6,
      "question": "What does 'self' refer to in a Python class method?",
      "options": ["The class itself", "The current instance of the class", "The parent class", "A global variable"],
      "correct_answer": 1,
      "difficulty": "medium",
      "explanation": "'self' refers to the specific instance of the class that is calling the method."
    },
    {
      "id": 7,
      "question": "Which method would you use to add an item to the END of a list?",
      "options": ["list.add()", "list.insert()", "list.append()", "list.push()"],
      "correct_answer": 2,
      "difficulty": "easy",
      "explanation": "The append() method adds a single element to the end of a list."
    },
    {
      "id": 8,
      "question": "What is a list comprehension?",
      "options": ["A way to understand lists", "A concise way to create lists", "A method to sort lists", "A list documentation tool"],
      "correct_answer": 1,
      "difficulty": "medium",
      "explanation": "List comprehensions provide a concise syntax to create new lists from existing iterables."
    },
    {
      "id": 9,
      "question": "What does the 'pass' statement do?",
      "options": ["Skips the current iteration", "Exits the program", "Does nothing (placeholder)", "Passes a variable to a function"],
      "correct_answer": 2,
      "difficulty": "easy",
      "explanation": "'pass' is a null operation used as a placeholder where code is syntactically required."
    },
    {
      "id": 10,
      "question": "Which operator checks if two variables point to the same object in memory?",
      "options": ["==", "is", "equals()", "==="],
      "correct_answer": 1,
      "difficulty": "hard",
      "explanation": "The 'is' operator checks identity (same object in memory), while '==' checks equality (same value)."
    }
  ]
}

Step 2: Build the Quiz Engine (Core Logic)

Now create quiz_engine.py. This file contains two classes: Question (represents a single question) and QuizEngine (manages the quiz flow).

"""
quiz_engine.py - Core quiz logic using Object-Oriented Programming
"""

import json
import random
import time
from datetime import datetime


class Question:
    """Represents a single quiz question with its options and metadata."""

    def __init__(self, data):
        self.id = data["id"]
        self.text = data["question"]
        self.options = data["options"]
        self.correct_answer = data["correct_answer"]
        self.difficulty = data["difficulty"]
        self.explanation = data["explanation"]

    def is_correct(self, user_answer):
        """Check if the user's answer (0-indexed) matches the correct answer."""
        return user_answer == self.correct_answer

    def display(self, question_number, total):
        """Print the question and its options in a formatted layout."""
        print(f"\n{'='*50}")
        print(f"  Question {question_number}/{total}  |  Difficulty: {self.difficulty.upper()}")
        print(f"{'='*50}")
        print(f"\n  {self.text}\n")

        for index, option in enumerate(self.options):
            print(f"    [{index}] {option}")
        print()


class QuizEngine:
    """Manages the quiz session: loading questions, scoring, and results."""

    def __init__(self, questions_file="questions.json"):
        self.questions_file = questions_file
        self.questions = []
        self.score = 0
        self.total_asked = 0
        self.answers_log = []
        self.start_time = None
        self.end_time = None

    def load_questions(self):
        """Load questions from the JSON file and create Question objects."""
        try:
            with open(self.questions_file, "r") as f:
                data = json.load(f)

            self.questions = [Question(q) for q in data["questions"]]
            print(f"  Loaded {len(self.questions)} questions successfully.")
            return True

        except FileNotFoundError:
            print(f"  Error: '{self.questions_file}' not found.")
            print("  Make sure the questions file is in the same directory.")
            return False
        except json.JSONDecodeError:
            print(f"  Error: '{self.questions_file}' contains invalid JSON.")
            return False

    def shuffle_questions(self):
        """Randomize the order of questions for each session."""
        random.shuffle(self.questions)

    def get_user_answer(self, num_options, time_limit=30):
        """
        Get and validate user input with an optional time limit.
        Returns the user's choice as an integer, or -1 if time expired.
        """
        start = time.time()

        while True:
            elapsed = time.time() - start
            remaining = time_limit - elapsed

            if remaining <= 0:
                print("\n  Time's up!")
                return -1

            try:
                prompt = f"  Your answer (0-{num_options - 1}) [{int(remaining)}s remaining]: "
                user_input = input(prompt).strip()

                if user_input.lower() == "q":
                    return -2  # Signal to quit

                choice = int(user_input)

                if 0 <= choice < num_options:
                    return choice
                else:
                    print(f"  Please enter a number between 0 and {num_options - 1}.")

            except ValueError:
                print("  Invalid input. Enter a number or 'q' to quit.")

    def run_quiz(self, num_questions=None, shuffle=True, time_limit=30):
        """Execute the main quiz loop."""
        if not self.load_questions():
            return

        if shuffle:
            self.shuffle_questions()

        # Allow limiting the number of questions
        if num_questions:
            self.questions = self.questions[:num_questions]

        total = len(self.questions)

        print(f"\n{'*'*50}")
        print(f"   PYTHON QUIZ - {total} Questions")
        print(f"   Time limit: {time_limit} seconds per question")
        print(f"   Type 'q' at any time to quit early")
        print(f"{'*'*50}")

        input("\n  Press Enter to start...")
        self.start_time = time.time()

        for index, question in enumerate(self.questions, 1):
            question.display(index, total)

            answer = self.get_user_answer(len(question.options), time_limit)

            if answer == -2:  # User quit
                print("\n  Quiz ended early.")
                break

            self.total_asked += 1

            if answer == -1:  # Time expired
                correct = False
            else:
                correct = question.is_correct(answer)

            if correct:
                self.score += 1
                print("  Correct!")
            else:
                correct_option = question.options[question.correct_answer]
                print(f"  Incorrect. The answer was: {correct_option}")

            print(f"  Explanation: {question.explanation}")

            # Log the answer for the results summary
            self.answers_log.append({
                "question": question.text,
                "user_answer": answer,
                "correct_answer": question.correct_answer,
                "was_correct": correct
            })

        self.end_time = time.time()
        self.show_results()
        self.save_score()

    def show_results(self):
        """Display the final quiz results with statistics."""
        if self.total_asked == 0:
            print("\n  No questions were answered.")
            return

        elapsed = self.end_time - self.start_time
        percentage = (self.score / self.total_asked) * 100

        print(f"\n{'='*50}")
        print(f"   QUIZ RESULTS")
        print(f"{'='*50}")
        print(f"   Score:      {self.score}/{self.total_asked} ({percentage:.1f}%)")
        print(f"   Time taken: {elapsed:.1f} seconds")
        print(f"   Average:    {elapsed/self.total_asked:.1f}s per question")
        print(f"{'='*50}")

        # Performance rating
        if percentage >= 90:
            print("   Rating: EXCELLENT! You really know your Python!")
        elif percentage >= 70:
            print("   Rating: GOOD! Solid understanding with room to grow.")
        elif percentage >= 50:
            print("   Rating: FAIR. Review the topics you missed.")
        else:
            print("   Rating: Keep practicing! Review Python fundamentals.")

        print()

    def save_score(self):
        """Save the quiz score to a persistent history file."""
        score_entry = {
            "date": datetime.now().strftime("%Y-%m-%d %H:%M"),
            "score": self.score,
            "total": self.total_asked,
            "percentage": round((self.score / self.total_asked) * 100, 1) if self.total_asked > 0 else 0
        }

        scores_file = "scores.json"

        try:
            with open(scores_file, "r") as f:
                history = json.load(f)
        except (FileNotFoundError, json.JSONDecodeError):
            history = []

        history.append(score_entry)

        with open(scores_file, "w") as f:
            json.dump(history, f, indent=2)

        print(f"  Score saved to {scores_file}")

    def show_history(self):
        """Display past quiz scores from the history file."""
        try:
            with open("scores.json", "r") as f:
                history = json.load(f)

            if not history:
                print("  No quiz history found.")
                return

            print(f"\n{'='*50}")
            print("   SCORE HISTORY")
            print(f"{'='*50}")
            print(f"   {'Date':<18} {'Score':<10} {'Percentage'}")
            print(f"   {'-'*40}")

            for entry in history[-10:]:  # Show last 10 attempts
                print(f"   {entry['date']:<18} {entry['score']}/{entry['total']:<7} {entry['percentage']}%")

            print()

        except FileNotFoundError:
            print("  No quiz history found. Complete a quiz first!")

Step 3: Create the Main Entry Point

Create main.py as the user-facing entry point with a menu system:

"""
main.py - Entry point for the Python Quiz Application
"""

from quiz_engine import QuizEngine


def display_menu():
    """Show the main menu options."""
    print(f"\n{'='*50}")
    print("   PYTHON QUIZ APP")
    print(f"{'='*50}")
    print("   [1] Start Quiz (All Questions)")
    print("   [2] Quick Quiz (5 Random Questions)")
    print("   [3] View Score History")
    print("   [4] Quit")
    print(f"{'='*50}")


def main():
    """Main application loop."""
    print("\n  Welcome to the Python Quiz App!")
    print("  Test your Python knowledge and track your progress.\n")

    while True:
        display_menu()
        choice = input("\n  Select an option (1-4): ").strip()

        if choice == "1":
            quiz = QuizEngine()
            quiz.run_quiz(shuffle=True, time_limit=30)

        elif choice == "2":
            quiz = QuizEngine()
            quiz.run_quiz(num_questions=5, shuffle=True, time_limit=20)

        elif choice == "3":
            quiz = QuizEngine()
            quiz.show_history()

        elif choice == "4":
            print("\n  Thanks for playing! Keep learning Python.\n")
            break

        else:
            print("  Invalid choice. Please enter 1, 2, 3, or 4.")


if __name__ == "__main__":
    main()

Step 4: Run and Test Your Quiz App

Open your terminal, navigate to the project folder, and run:

cd python_quiz_app
python main.py

Expected output:

  Welcome to the Python Quiz App!
  Test your Python knowledge and track your progress.

==================================================
   PYTHON QUIZ APP
==================================================
   [1] Start Quiz (All Questions)
   [2] Quick Quiz (5 Random Questions)
   [3] View Score History
   [4] Quit
==================================================

  Select an option (1-4): 

Step 5: Add More Features (Stretch Goals)

Once the basic version works, enhance your quiz app with these additions:

Feature: Difficulty Filtering

Allow users to choose a difficulty level before starting:

def filter_by_difficulty(self, difficulty):
    """Filter questions to only include a specific difficulty level."""
    self.questions = [q for q in self.questions if q.difficulty == difficulty]
    print(f"  Filtered to {len(self.questions)} {difficulty} questions.")

Feature: Categories

Add a “category” field to your JSON questions and let users pick topics like “Data Types”, “Functions”, “OOP”, or “File Handling”.

Feature: Leaderboard

If multiple people use the app, add a username field to scores and display a ranked leaderboard.

Feature: GUI Version with Tkinter

Once you are comfortable with the CLI version, convert it to a graphical application using Tkinter. You can use buttons for answer selection and labels for the question display.


Complete Project Summary

Here is what you built and the skills you practiced:

ComponentSkills Practiced
questions.jsonJSON data format, data/code separation
Question classOOP, encapsulation, clean method design
QuizEngine classFile I/O, error handling, state management
main.pyMenu systems, user input loops, code organization
Timer featuretime module, real-time constraints
Score persistenceJSON read/write, data appending

FAQ

How do I add more questions to the quiz?

Open questions.json and add new question objects to the “questions” array. Follow the same format: include “id”, “question”, “options” (as a list), “correct_answer” (0-indexed), “difficulty”, and “explanation”. No code changes needed.

Can I turn this quiz app into a web application?

Yes. The QuizEngine class you built here can be reused almost entirely. Replace the input() and print() calls with Flask routes and HTML templates. The JSON data loading and scoring logic remains identical.

What if I want to load questions from an API instead of a file?

Replace the load_questions method’s file-reading logic with an HTTP request using the requests library. Parse the JSON response the same way you parse the file. Everything downstream works without changes because the Question class accepts any dictionary with the right keys.

How can I prevent cheating in a multiplayer version?

For a local multiplayer quiz, shuffle questions and options differently for each player. For a networked version, keep the correct answers on the server side and only send question text and options to clients. Validate answers server-side.


Next Steps

Now that you know how to build a Python quiz app step by step, try these extensions:

  1. Add a “review mistakes” mode that replays only questions you got wrong
  2. Implement spaced repetition (show questions you struggle with more often)
  3. Create a question editor mode where users can add new questions through the CLI
  4. Deploy it as a web app using Flask or FastAPI

For more Python project ideas, check out our complete list of Python project ideas for beginners 2026.


Last updated: July 2026

Leave a Comment

Scroll to Top