Home » Tutorials » How to Build a Quiz Game with Python and Tkinter

How to Build a Quiz Game with Python and Tkinter

Building interactive applications can be both fun and educational. One exciting project you can undertake is creating a quiz game. Quiz games are not only entertaining but also provide a great way to test knowledge on various topics, making them perfect for learning and teaching purposes. Whether you’re a beginner or an experienced programmer, developing a quiz game allows you to practice your Python skills while creating something engaging and useful.

Today, you’ll learn how to build a quiz game with Python and Tkinter. In this tutorial, we’ll cover fetching questions from an online API, handling user input, managing scores, and creating a dynamic user interface. By the end of this article, you’ll have a fully functional quiz game that you can customize and expand to suit your needs. Let’s get started!

Table of Contents

Getting Started

For the code to function properly, Install these libraries by running the following commands:

$ pip install tk 
$ pip install requests
$ pip install pillow

Imports

import tkinter as tk
from tkinter import messagebox, simpledialog, ttk
import requests
import random
import time
import json
import os

As usual, we are about to start an exciting journey by importing the essential libraries:

  • tkinter: We’ll use this to create our graphical user interface, handle message boxes with messagebox, manage user interactions with simpledialog, and style our widgets with ttk.
  • requests: This library helps us fetch questions from online APIs.
  • random: We need this to generate random numbers and shuffle our quiz options.
  • time: We’ll use this to keep track of our quiz timer.
  • json: This is crucial for parsing the JSON data we receive from the API.
  • os: This library allows us to interact with the operating system, especially for saving and loading high scores.

With these tools in hand, we’re ready to dive into building our quiz game!

Fetching Quiz Questions from an API

Our goal is to create a quiz game, and what is a quiz without questions? To solve this, we created the fetch_questions() function, which acts as our question gatherer:

First, we set up parameters: “the number of questions, category ID, difficulty level, and question type“. The function uses these parameters to form a URL for the API endpoint and sends a GET request with requests.get(url). It checks for any errors in the HTTP request and, upon success, parses the JSON response to retrieve the list of questions. If the request fails, an error message will pop up.

You can find more details about the API we used here.

# We start with Fetching questions from the Open Trivia Database API
def fetch_questions(amount=10, category=9, difficulty='medium', qtype='multiple'):
   url = f"https://opentdb.com/api.php?amount={amount}&category={category}&difficulty={difficulty}&type={qtype}"
   try:
       response = requests.get(url)
       response.raise_for_status()
       return response.json().get('results', [])
   except requests.RequestException as e:
       messagebox.showerror("Error", f"Failed to fetch questions: {e}")
       return []

Saving High Scores for the Quiz Game

# Save high scores to a file
def save_high_scores(scores):
   with open('high_scores.json', 'w') as file:
       json.dump(scores, file)


# Load high scores from a file
def load_high_scores():
   if os.path.exists('high_scores.json'):
       with open('high_scores.json', 'r') as file:
           return json.load(file)
   return []

What’s the point of a game if no one knows you conquered it? That’s why we created the load_high_scores() function. This function checks if the high_scores.json file exists using os.path.exists(). If it does, the function opens the file, reads the scores using json.load(file), and returns them as a list. Additionally, the save_high_scores() function saves the current scores to high_scores.json by writing the scores with json.dump(scores, file).

Displaying and Resetting High Scores

Welcome to the hall of fame, where the show_high_scores() function honors the champions by formatting and displaying high scores in an info message box.

# Show high scores
def show_high_scores():
   global high_scores
   high_scores_text = "\n".join([f"{i+1}. {score['name']} - {score['score']}" for i, score in enumerate(high_scores)])
   messagebox.showinfo("High Scores", high_scores_text)

This hall of fame list isn’t permanent. The reset_high_scores() function can clear the current champions from the list by emptying the high score list and saving it. Once the reset is successful, a message box will notify you that the high scores list has been reset.

# Reset high scores
def reset_high_scores():
   global high_scores
   high_scores = []
   save_high_scores(high_scores)
   messagebox.showinfo("High Scores", "High scores have been reset.")

Updating the Quiz Timer

Now, let’s add some pressure to our quiz game with a timer. The update_timer() function sets the timer to 20 seconds and calculates the remaining time by subtracting the elapsed time from the initial 20 seconds. It also updates the timer_label with each passing second. If the 20 seconds run out, it calls the check_answer() function with time_up set to True.

# Update the timer
def update_timer():
   global start_time, score_multiplier, quiz_active
   if not quiz_active:
       return
   elapsed_time = time.time() - start_time
   remaining_time = 20 - int(elapsed_time)
   if remaining_time >= 0:
       timer_label.config(text=f"Time left: {remaining_time}s")
       root.after(1000, update_timer)
   else:
       
       check_answer(time_up=True)

Answer Validation and Final Score Display

For this step, we need to ensure our quiz game can check answers and show the final score, so we created these two functions:

The check_answer() function determines if the selected answer is correct. It retrieves the chosen option with options.get(), compares its text to the correct answer, and updates the score and multiplier accordingly. If correct, it adds 1 to the score and multiplier, displaying a success message. If incorrect, it resets the multiplier and shows the correct answer. Regardless, it calls next_question() to load the next question.

# Check the answer
def check_answer(time_up=False):
   global score, score_multiplier, quiz_active
   if not quiz_active:
       return
   if not time_up:
       selected_option = options.get()
       selected_text = option_buttons[int(selected_option)].cget("text")
       if selected_text == correct_answer:
           score += score_multiplier
           score_multiplier += 1
           messagebox.showinfo("Result", "Correct!", icon='info')
       else:
           score_multiplier = 1
           messagebox.showinfo("Result", f"Incorrect! The correct answer is {correct_answer}.", icon='error')
   next_question()

Next, to display the final score, we define the show_score() function, which activates when the player finishes the quiz. It stops the quiz by setting quiz_active to False, displays the player’s final score in a message box, and asks for their name using simpledialog. The score is added to the high scores list, which keeps only the top 5 scores, saves them, and displays them.

# Show the final score
def show_score():
   global score, high_scores, quiz_active
   quiz_active = False
   messagebox.showinfo("Your Quiz is Over", f"You Have Answered {score} out of {len(questions)}")
   name = simpledialog.askstring("High Scores", "Please Enter your name for the high score list:")
   if name:
       high_scores.append({"name": name, "score": score})
       high_scores.sort(key=lambda x: x["score"], reverse=True)
       high_scores = high_scores[:5]
       save_high_scores(high_scores)
       show_high_scores()

Loading the Next Quiz Question

# Load the next question
def next_question():
   global question_index, start_time, correct_answer, score, quiz_active
   if not quiz_active:
       return
   if question_index < len(questions):
       q = questions[question_index]
       question_label.config(text=q["question"])
       choices = q["incorrect_answers"] + [q["correct_answer"]]
       random.shuffle(choices)
       for i, choice in enumerate(choices):
           option_buttons[i].config(text=choice)
       correct_answer = q["correct_answer"]
       question_index += 1
       start_time = time.time()
       score_multiplier_label.config(text=f"Score Multiplier: x{score_multiplier}")
       update_timer()
   else:
       show_score()

We mentioned the next_question() function earlier, but let’s dive into how it actually works:

  • This function springs into action only if the quiz is active, checked through quiz_active. It then sees if there are more questions left. If there are, it loads the next question by updating the question text and shuffling the answer choices. It also updates the correct answer, increments the question index, sets the start time, and updates the score multiplier.
  • Finally, it calls update_timer(). This cycle repeats until no more questions remain, then it calls show_score().

Starting and Resetting the Quiz

We’ve finally reached the start_quiz() function, which kicks off the quiz game. Here’s how it works:

First, it retrieves the selected category and difficulty, then calls fetch_questions() to get the questions. After fetching, it initializes the score, score multiplier, and question index, and sets quiz_active to True. Then, it calls next_question() to load the first question, starting the quiz. If anything goes wrong, an error message will appear.

# Start quiz with selected category and difficulty
def start_quiz():
   global questions, score, question_index, score_multiplier, quiz_active
   selected_category_name = category_var.get()
   category_id = categories[selected_category_name]
   difficulty = difficulty_var.get()
   questions = fetch_questions(amount=10, category=category_id, difficulty=difficulty)
   if questions:
       score = 0
       question_index = 0
       score_multiplier = 1
       quiz_active = True
       next_question()
   else:
       messagebox.showerror("Error", "Failed to load questions. Please try again.")

As gamers, we know that sometimes you might want a do-over. That’s where the reset_quiz() function comes in. It resets the score, question list, question index, and score multiplier, and sets quiz_active to False. It also clears the question text and resets the timer and score multiplier labels, giving you a clean slate to start fresh.

# Reset the quiz
def reset_quiz():
   global score, question_index, questions, score_multiplier, quiz_active
   score = 0
   question_index = 0
   questions = []
   score_multiplier = 1
   quiz_active = False
   question_label.config(text="")
   for btn in option_buttons:
       btn.config(text="")
   timer_label.config(text="Time left: 20s")
   score_multiplier_label.config(text="Score Multiplier: x1")

Initializing Global Variables

Now we come to the core of our script: the global variables. Let’s go through them:

  • score: Keeps track of your current points.
  • question_index: Tells us which question you’re on.
  • questions: Holds all the quiz questions.
  • high_scores: Stores the top scores from past games.
  • score_multiplier: Adds excitement by increasing your score multiplier.
  • quiz_active: A simple true/false that shows if the quiz is currently running.
# Initialize global variables
score = 0
question_index = 0
questions = []
high_scores = load_high_scores()
score_multiplier = 1
quiz_active = False

Main Window Setup

We’ve reached the grand finale, where we bring all the previous elements together into one graphical interface. Let’s break it down step by step:

  • Creating the Main Window: We start by creating the main window, setting its title, and defining its size and shape.
  • Adding a Menu Bar: We create a menu bar with tk.Menu(root) and set it up for the main window. In this menu bar, we add a ‘High Scores‘ submenu linked to two commands: show_high_scores() and reset_high_scores().
  • Timer and Question Display: Next, we create a label (timer_label) to show the timer and another label (question_label) to display the question.
  • Answer Options: For the answer options, we create a list (option_buttons) to store the option buttons and a StringVar to hold the selected option. We then create four radio buttons, each linked to the StringVar, and add them to the list.
  • Submit Button: We add a “Submit” button that calls check_answer(time_up=False) when clicked.
  • Score Multiplier and Drop-down Menus: We create a label for the score multiplier. Then, we add two drop-down menus: one for categories (with a category_var to store the selected category) and one for difficulty levels (with a difficulty_var to store the selected difficulty).
  • Control Buttons: Finally, we create a frame for buttons (button_frame). Inside it, we add a “Start Quiz” button that calls start_quiz() and a “Reset Quiz” button that calls reset_quiz().
  • Running the App: To make the app responsive, we start the main event loop with mainloop().

Now, everything is set up to create an engaging and interactive quiz game interface!

# Set up the Tkinter window
root = tk.Tk()
root.title("Quiz Game - The Pycodes")
root.geometry("500x660")


menu = tk.Menu(root)
root.config(menu=menu)
high_score_menu = tk.Menu(menu)
menu.add_cascade(label="High Scores", menu=high_score_menu)
high_score_menu.add_command(label="View High Scores", command=show_high_scores)
high_score_menu.add_command(label="Reset High Scores", command=reset_high_scores)


timer_label = tk.Label(root, text="Time left: 20s", font=("Helvetica", 14))
timer_label.pack(pady=20)


question_label = tk.Label(root, text="", wraplength=400, font=("Helvetica", 16))
question_label.pack(pady=20)


options = tk.StringVar()
option_buttons = []
for i in range(4):
   btn = tk.Radiobutton(root, text="", variable=options, value=i, font=("Helvetica", 14))
   btn.pack(anchor="w")
   option_buttons.append(btn)


submit_button = tk.Button(root, text="Submit", command=lambda: check_answer(time_up=False), font=("Helvetica", 14))
submit_button.pack(pady=20)


score_multiplier_label = tk.Label(root, text="Score Multiplier: x1", font=("Helvetica", 14))
score_multiplier_label.pack(pady=20)


category_label = tk.Label(root, text="Select Category:", font=("Helvetica", 14))
category_label.pack(pady=10)
categories = {
   "General Knowledge": 9,
   "Science & Nature": 17,
   "Sports": 21,
   "Geography": 22,
   "History": 23,
   "Art": 25,
   "Celebrities": 26
}
category_var = tk.StringVar(value="General Knowledge")
category_menu = ttk.Combobox(root, textvariable=category_var, values=list(categories.keys()), state="readonly")
category_menu.pack(pady=10)


difficulty_label = tk.Label(root, text="Select Difficulty:", font=("Helvetica", 14))
difficulty_label.pack(pady=10)
difficulties = ["easy", "medium", "hard"]
difficulty_var = tk.StringVar(value="medium")
difficulty_menu = ttk.Combobox(root, textvariable=difficulty_var, values=difficulties, state="readonly")
difficulty_menu.pack(pady=10)


# Now We Create a frame for the start and stop buttons
button_frame = tk.Frame(root)
button_frame.pack(pady=20)


start_button = tk.Button(button_frame, text="Start Quiz", command=start_quiz, font=("Helvetica", 14))
start_button.pack(side="left", padx=10)


reset_button = tk.Button(button_frame, text="Reset Quiz", command=reset_quiz, font=("Helvetica", 14))
reset_button.pack(side="left", padx=10)


root.mainloop()

Example

I played this game on a Windows system as shown in the image below:

Also on a Linux system:

Full Code

import tkinter as tk
from tkinter import messagebox, simpledialog, ttk
import requests
import random
import time
import json
import os


# We start with Fetching questions from the Open Trivia Database API
def fetch_questions(amount=10, category=9, difficulty='medium', qtype='multiple'):
   url = f"https://opentdb.com/api.php?amount={amount}&category={category}&difficulty={difficulty}&type={qtype}"
   try:
       response = requests.get(url)
       response.raise_for_status()
       return response.json().get('results', [])
   except requests.RequestException as e:
       messagebox.showerror("Error", f"Failed to fetch questions: {e}")
       return []


# Save high scores to a file
def save_high_scores(scores):
   with open('high_scores.json', 'w') as file:
       json.dump(scores, file)


# Load high scores from a file
def load_high_scores():
   if os.path.exists('high_scores.json'):
       with open('high_scores.json', 'r') as file:
           return json.load(file)
   return []


# Show high scores
def show_high_scores():
   global high_scores
   high_scores_text = "\n".join([f"{i+1}. {score['name']} - {score['score']}" for i, score in enumerate(high_scores)])
   messagebox.showinfo("High Scores", high_scores_text)


# Reset high scores
def reset_high_scores():
   global high_scores
   high_scores = []
   save_high_scores(high_scores)
   messagebox.showinfo("High Scores", "High scores have been reset.")


# Update the timer
def update_timer():
   global start_time, score_multiplier, quiz_active
   if not quiz_active:
       return
   elapsed_time = time.time() - start_time
   remaining_time = 20 - int(elapsed_time)
   if remaining_time >= 0:
       timer_label.config(text=f"Time left: {remaining_time}s")
       root.after(1000, update_timer)
   else:
       
       check_answer(time_up=True)


# Check the answer
def check_answer(time_up=False):
   global score, score_multiplier, quiz_active
   if not quiz_active:
       return
   if not time_up:
       selected_option = options.get()
       selected_text = option_buttons[int(selected_option)].cget("text")
       if selected_text == correct_answer:
           score += score_multiplier
           score_multiplier += 1
           messagebox.showinfo("Result", "Correct!", icon='info')
       else:
           score_multiplier = 1
           messagebox.showinfo("Result", f"Incorrect! The correct answer is {correct_answer}.", icon='error')
   next_question()


# Show the final score
def show_score():
   global score, high_scores, quiz_active
   quiz_active = False
   messagebox.showinfo("Your Quiz is Over", f"You Have Answered {score} out of {len(questions)}")
   name = simpledialog.askstring("High Scores", "Please Enter your name for the high score list:")
   if name:
       high_scores.append({"name": name, "score": score})
       high_scores.sort(key=lambda x: x["score"], reverse=True)
       high_scores = high_scores[:5]
       save_high_scores(high_scores)
       show_high_scores()


# Load the next question
def next_question():
   global question_index, start_time, correct_answer, score, quiz_active
   if not quiz_active:
       return
   if question_index < len(questions):
       q = questions[question_index]
       question_label.config(text=q["question"])
       choices = q["incorrect_answers"] + [q["correct_answer"]]
       random.shuffle(choices)
       for i, choice in enumerate(choices):
           option_buttons[i].config(text=choice)
       correct_answer = q["correct_answer"]
       question_index += 1
       start_time = time.time()
       score_multiplier_label.config(text=f"Score Multiplier: x{score_multiplier}")
       update_timer()
   else:
       show_score()


# Start quiz with selected category and difficulty
def start_quiz():
   global questions, score, question_index, score_multiplier, quiz_active
   selected_category_name = category_var.get()
   category_id = categories[selected_category_name]
   difficulty = difficulty_var.get()
   questions = fetch_questions(amount=10, category=category_id, difficulty=difficulty)
   if questions:
       score = 0
       question_index = 0
       score_multiplier = 1
       quiz_active = True
       next_question()
   else:
       messagebox.showerror("Error", "Failed to load questions. Please try again.")


# Reset the quiz
def reset_quiz():
   global score, question_index, questions, score_multiplier, quiz_active
   score = 0
   question_index = 0
   questions = []
   score_multiplier = 1
   quiz_active = False
   question_label.config(text="")
   for btn in option_buttons:
       btn.config(text="")
   timer_label.config(text="Time left: 20s")
   score_multiplier_label.config(text="Score Multiplier: x1")


# Initialize global variables
score = 0
question_index = 0
questions = []
high_scores = load_high_scores()
score_multiplier = 1
quiz_active = False


# Set up the Tkinter window
root = tk.Tk()
root.title("Quiz Game - The Pycodes")
root.geometry("500x660")


menu = tk.Menu(root)
root.config(menu=menu)
high_score_menu = tk.Menu(menu)
menu.add_cascade(label="High Scores", menu=high_score_menu)
high_score_menu.add_command(label="View High Scores", command=show_high_scores)
high_score_menu.add_command(label="Reset High Scores", command=reset_high_scores)


timer_label = tk.Label(root, text="Time left: 20s", font=("Helvetica", 14))
timer_label.pack(pady=20)


question_label = tk.Label(root, text="", wraplength=400, font=("Helvetica", 16))
question_label.pack(pady=20)


options = tk.StringVar()
option_buttons = []
for i in range(4):
   btn = tk.Radiobutton(root, text="", variable=options, value=i, font=("Helvetica", 14))
   btn.pack(anchor="w")
   option_buttons.append(btn)


submit_button = tk.Button(root, text="Submit", command=lambda: check_answer(time_up=False), font=("Helvetica", 14))
submit_button.pack(pady=20)


score_multiplier_label = tk.Label(root, text="Score Multiplier: x1", font=("Helvetica", 14))
score_multiplier_label.pack(pady=20)


category_label = tk.Label(root, text="Select Category:", font=("Helvetica", 14))
category_label.pack(pady=10)
categories = {
   "General Knowledge": 9,
   "Science & Nature": 17,
   "Sports": 21,
   "Geography": 22,
   "History": 23,
   "Art": 25,
   "Celebrities": 26
}
category_var = tk.StringVar(value="General Knowledge")
category_menu = ttk.Combobox(root, textvariable=category_var, values=list(categories.keys()), state="readonly")
category_menu.pack(pady=10)


difficulty_label = tk.Label(root, text="Select Difficulty:", font=("Helvetica", 14))
difficulty_label.pack(pady=10)
difficulties = ["easy", "medium", "hard"]
difficulty_var = tk.StringVar(value="medium")
difficulty_menu = ttk.Combobox(root, textvariable=difficulty_var, values=difficulties, state="readonly")
difficulty_menu.pack(pady=10)


# Now We Create a frame for the start and stop buttons
button_frame = tk.Frame(root)
button_frame.pack(pady=20)


start_button = tk.Button(button_frame, text="Start Quiz", command=start_quiz, font=("Helvetica", 14))
start_button.pack(side="left", padx=10)


reset_button = tk.Button(button_frame, text="Reset Quiz", command=reset_quiz, font=("Helvetica", 14))
reset_button.pack(side="left", padx=10)


root.mainloop()

Happy Coding!

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top