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
- Imports
- Fetching Quiz Questions from an API
- Saving High Scores for the Quiz Game
- Displaying and Resetting High Scores
- Updating the Quiz Timer
- Answer Validation and Final Score Display
- Loading the Next Quiz Question
- Starting and Resetting the Quiz
- Initializing Global Variables
- Main Window Setup
- Example
- Full Code
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 withsimpledialog
, and style our widgets withttk
. - 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 callsshow_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()
andreset_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 aStringVar
to hold the selected option. We then create four radio buttons, each linked to theStringVar
, 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 adifficulty_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 callsstart_quiz()
and a “Reset Quiz” button that callsreset_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!