Build A Quiz Application With Python
Build A Quiz Application With Python
Email…
advanced
api
basics
best-practices
community
databases
data-science
Prerequisites testing
tools
web-dev
web-scraping
Remove ads
In this tutorial, you’ll build a Python quiz application for the terminal. The word quiz
was first used in 1781 to mean eccentric person. Nowadays, it’s mostly used to describe
short tests of trivia or expert knowledge with questions like the following:
By following along in this step-by-step project, you’ll build an application that can test a
person’s expertise on a range of topics. You can use this project to strengthen your own
knowledge or to challenge your friends to a fun battle of wits.
The quiz application is a comprehensive project for anyone comfortable with the basics
of Python. Throughout the tutorial, you’ll get all the code you need in separate, bite-size
steps. You can also find the full source code of the application by clicking on the link
below:
Get Source Code: Click here to get access to the source code that you’ll use to
build your quiz application.
Whether you’re an eccentric person or not, read on to learn how to create your own quiz.
You first choose a topic for your questions. Then, for each question, you’ll choose an
answer from a set of alternatives. Some questions may have multiple correct answers.
You can access a hint to help you along the way. After answering a question, you’ll read
an explanation that can provide more context for the answer.
Remove ads
Project Overview
You’ll start by creating a basic Python quiz application that’s only capable of asking a
question, collecting an answer, and checking whether the answer is correct. From there,
you’ll add more and more features in order to make your app more interesting, user-
friendly, and fun.
You’ll build the quiz application iteratively by going through the following steps:
As you follow along, you’ll gain experience in starting with a small script and expanding
it. This is an important skill in and of itself. Your favorite program, app, or game probably
started as a small proof of concept that later grew into what it is today.
Prerequisites
In this tutorial, you’ll build a quiz application using Python’s basic building blocks. While
working through the steps, it’s helpful if you’re comfortable with the following concepts:
If you’re not confident in your knowledge of these prerequisites, then that’s okay too! In
fact, going through this tutorial will help you practice these concepts. You can always
stop and review the resources linked above if you get stuck.
Get Source Code: Click here to get access to the source code that you’ll use to
build your quiz application.
Python >>>
>>> name
'Geir Arne'
input() takes an optional prompt that’s displayed to the user before the user enters
information. In the example above, the prompt is shown in the highlighted line, and the
user enters Geir Arne before hitting Enter ↩ . Whatever the user enters is returned
from input(). This is seen in the REPL example, as the string 'Geir Arne' has been
assigned to name.
You can use input() to have Python ask you questions and check your answers. Try the
following:
Python >>>
>>> answer = input("When was the first known use of the word 'quiz'? ")
When was the first known use of the word 'quiz'? 1781
False
True
This example shows one thing that you need to be aware of: input() always returns a
text string, even if that string contains only digits. As you’ll soon see, this won’t be an
issue for the quiz application. However, if you wanted to use the result of input() for
mathematical calculations, then you’d need to convert it first.
Time to start building your quiz application. Open your editor and create the file quiz.py
with the following content:
Python
# quiz.py
answer = input("When was the first known use of the word 'quiz'? ")
if answer == "1781":
print("Correct!")
else:
This code is very similar to what you experimented with in the REPL above. You can run
your application to check your knowledge:
Shell
$ python quiz.py
When was the first known use of the word 'quiz'? 1871
If you happen to give the wrong answer, then you’ll be gently corrected so that you’ll
hopefully do better next time.
Note: The f before your quoted string literal inside the else clause indicates that
the string is a formatted string, usually called an f-string. Python evaluates
expressions inside curly braces ({}) within f-strings and inserts them into the string.
You can optionally add different format specifiers.
For example, !r indicates that answer should be inserted based on its repr()
representation. In practice, this means that strings are shown surrounded by single
quotes, like '1871'.
A quiz with only one question isn’t very exciting! You can ask another question by
repeating your code:
Python
# quiz.py
answer = input("When was the first known use of the word 'quiz'? ")
if answer == "1781":
print("Correct!")
else:
answer = input("Which built-in function can get information from the user? "
if answer == "input":
print("Correct!")
else:
You’ve added a question by copying and pasting the previous code then changing the
question text and the correct answer. Again, you can test this by running the script:
Shell
$ python quiz.py
When was the first known use of the word 'quiz'? 1781
Correct!
Which built-in function can get information from the user? get
It works! However, copying and pasting code like this isn’t great. There’s a programming
principle called Don’t Repeat Yourself (DRY), which says that you should usually avoid
repeated code because it gets hard to maintain.
Next, you’ll start improving your code to make it easier to work with.
Remove ads
Instead of repeating code, you’ll treat your questions and answers as data and move
them into a data structure that your code can loop over. The immediate—and often
challenging—question then becomes how you should structure your data.
There’s never one uniquely perfect data structure. You’ll usually choose between several
alternatives. Throughout this tutorial, you’ll revisit your choice of data structure several
times as your application grows.
Python
("When was the first known use of the word 'quiz'", "1781"),
("Which built-in function can get information from the user", "input"),
]
This fits nicely with how you want to use your data. You’ll loop over each question, and
for each question, you want access to both the question and answer.
Change your quiz.py file so that you store your questions and answers in the QUESTIONS
data structure:
Python
# quiz.py
QUESTIONS = [
("When was the first known use of the word 'quiz'", "1781"),
("Which built-in function can get information from the user", "input"),
("Which keyword do you use to loop over a given list of elements", "for"
]
if answer == correct_answer:
print("Correct!")
else:
When you run this code, it shouldn’t look any different from how it did earlier. In fact, you
haven’t added any new functionality. Instead, you’ve refactored your code so that it’ll be
easier to add more questions to your application.
In the previous version of your code, you needed to add five new lines of code for each
question that you added. Now, the for loop takes care of running those five lines for each
question. To add a new question, you only need to add one line spelling out the question
and the corresponding answer.
Note: You’re learning about quizzes in this tutorial, so questions and answers are
important. Each code example will introduce a new question. To keep the code
listings in this tutorial at a manageable size, some of the older questions may be
removed. However, feel free to keep all questions in your code, or to even replace
these with your own questions and answers.
The questions you’ll see in the examples are related to the tutorial, even though
you won’t find all the answers in the text. Feel free to search online if you’re curious
about more details about a question or an answer.
Next, you’ll make your quiz application easier to use by adding answer alternatives for
each question.
Shell
Which built-in function can get information from the user? input()
Should they really be marked wrong because they included the parentheses to indicate
that the function is callable? You can take away a lot of guesswork for the users by giving
them alternatives. For example:
Shell
- get
- input
- write
Which built-in function can get information from the user? input
Correct!
Here, the alternatives show that you expect the answer to be entered without
parentheses. In the example, the alternatives are listed before the question. This is a bit
counterintuitive, but it’s easier to implement into your current code. You’ll improve this
in the next step.
In order to implement answer alternatives, you need your data structure to be able to
record three pieces of information for each question:
It’s time to revisit QUESTIONS for the first—but not the last—time and make some changes
to it. It makes sense to store the answer alternatives in a list, as there can be any number
of them and you just want to display them to the screen. Furthermore, you can treat the
correct answer as one of the answer alternatives and include it in the list, as long as
you’re able to retrieve it later.
You decide to change QUESTIONS to a dictionary where the keys are your questions and
the values are the lists of answer alternatives. You consistently put the correct answer as
the first item in the list of alternatives so that you can identify it.
Note: You could continue to use a list of two-tuples to hold your questions. In fact,
you’re only iterating over the questions and answers, not looking up the answers by
using a question as a key. Therefore, you could argue that the list of tuples is a
better data structure for your use case than a dictionary.
However, you use a dictionary because it looks better visually in your code, and the
roles of questions and answer alternatives are more distinct.
You update your code to loop over each item in your newly minted dictionary. For each
question, you pick out the correct answer from the alternatives, and you print out all the
alternatives before asking the question:
Python
# quiz.py
QUESTIONS = {
],
],
],
],
correct_answer = alternatives[0]
print(f" - {alternative}")
if answer == correct_answer:
print("Correct!")
else:
If you always showed the correct answer as the first alternative, then your users would
soon catch on and be able to guess the correct answer every time. Instead, you change
the order of the alternatives by sorting them. Test your application:
Shell
$ python quiz.py
- 1771
- 1781
- 1871
- 1881
When was the first known use of the word 'quiz'? 1781
Correct!
...
The answer is 'To iterate over two or more sequences at the same time',
not 'To itertate over two or more sequences at the same time'
The last question reveals another experience that can be frustrating for the user. In this
example, they’ve chosen the correct alternative. However, as they were typing it, a typo
snuck in. Can you make your application more forgiving?
You know that the user will answer with one of the alternatives, so you just need a way
for them to communicate which alternative they choose. You can add a label to each
alternative and only ask the user to enter the label.
Update the application to use enumerate() to print the index of each answer alternative:
Python
# quiz.py
QUESTIONS = {
],
],
],
correct_answer = alternatives[0]
sorted_alternatives = sorted(alternatives)
answer = sorted_alternatives[answer_label]
if answer == correct_answer:
print("Correct!")
else:
You store the reordered alternatives as sorted_alternatives so that you can look up the
full answer based on the answer label that the user enters. Recall that input() always
returns a string, so you need to convert it to an integer before you treat it as a list index.
Shell
$ python quiz.py
0) each
1) for
2) loop
3) while
Correct!
0) Bubble sort
1) Merge sort
2) Quicksort
3) Timsort
Correct!
Great! You’ve created quite a capable quiz application! In the next step, you won’t add
any more functionality. Instead, you’ll make your application more user-friendly.
Remove ads
Your program will still work similarly to now, but it’ll be more robust and attractive. You
can find the source code as it’ll look at the end of this step in the source_code_step_2
directory by clicking below:
Get Source Code: Click here to get access to the source code that you’ll use to
build your quiz application.
In your next update to quiz.py, you’ll number the questions themselves and present the
question text above the answer alternatives. Additionally, you’ll use lowercase letters
instead of numbers to identify answers:
Python
# quiz.py
QUESTIONS = {
],
],
print(f"\nQuestion {num}:")
print(f"{question}?")
correct_answer = alternatives[0]
answer = labeled_alternatives.get(answer_label)
if answer == correct_answer:
print(" ⭐ Correct! ⭐
")
else:
You use string.ascii_lowercase to get letters that label your answer alternatives. You
combine letters and alternatives with zip() and store them in a dictionary as follows:
Python >>>
You use these labeled alternatives when you display the options to the user and when
you look up the user’s answer based on the label that they entered. Note the use of the
special escape string "\n". This is interpreted as a newline and adds a blank line on the
screen. This is a simple way to add some organization to your output:
Shell
$ python quiz.py
Question 1:
Choice? d
⭐ Correct! ⭐
Question 2:
a) Bubble sort
b) Merge sort
c) Quicksort
d) Timsort
Choice? c
Your output is still mostly monochrome in the terminal, but it’s more visually pleasing,
and it’s easier to read.
Keep Score
Now that you’re numbering the questions, it would also be nice to keep track of how
many questions the user answers correctly. You can add a variable, num_correct, to take
care of this:
Python
# quiz.py
QUESTIONS = {
],
"enumerate(iterable)",
"enumerate(iterable, start=1)",
"range(iterable)",
"range(iterable, start=1)",
],
num_correct = 0
print(f"\nQuestion {num}:")
print(f"{question}?")
correct_answer = alternatives[0]
answer = labeled_alternatives.get(answer_label)
if answer == correct_answer:
num_correct += 1
else:
You increase num_correct for each correct answer. The num loop variable already counts
the total number of questions, so you can use that to report the user’s result.
Remove ads
You can handle user errors in a better way by allowing the user to re-enter their answer
when they enter something invalid. One way to do this is to wrap input() in a while
loop:
Python >>>
...
Hello!
Echo: Hello!
Walrus ...
quit
The condition (text := input()) != "quit" does a few things at once. It uses an
assigment expression (:=), often called the walrus operator, to store the user input as
text and compare it to the string "quit". The while loop will run until you type quit at
the prompt. See The Walrus Operator: Python 3.8 Assignment Expressions for more
examples.
In your quiz application, you use a similar construct to loop until the user gives a valid
answer:
Python
# quiz.py
QUESTIONS = {
"enumerate(iterable)",
"enumerate(iterable, start=1)",
"range(iterable)",
"range(iterable, start=1)",
],
"Assignment expression",
"Named expression",
"Walrus operator",
],
num_correct = 0
print(f"\nQuestion {num}:")
print(f"{question}?")
correct_answer = alternatives[0]
answer = labeled_alternatives[answer_label]
if answer == correct_answer:
num_correct += 1
else:
If you enter an invalid choice at the prompt, then you’ll be reminded about your valid
choices:
Shell
$ python quiz.py
Question 1:
a) enumerate(iterable)
b) enumerate(iterable, start=1)
c) range(iterable)
d) range(iterable, start=1)
Choice? e
Choice? a
⭐ Correct! ⭐
Note that once the while loops exits, you’re guaranteed that answer_label is one of the
keys in labeled_alternatives, so it’s safe to look up answer directly. Next, you’ll add
one more improvement by injecting some randomness into your quiz.
You can add some variety to your quiz by changing things up a little. You can randomize
both the order of the questions and the order of the answer alternatives for each
question:
Python
# quiz.py
import random
NUM_QUESTIONS_PER_QUIZ = 5
QUESTIONS = {
"Assignment expression",
"Named expression",
"Walrus operator",
],
num_correct = 0
print(f"\nQuestion {num}:")
print(f"{question}?")
correct_answer = alternatives[0]
labeled_alternatives = dict(
answer = labeled_alternatives[answer_label]
if answer == correct_answer:
num_correct += 1
else:
You use random.sample() to randomize the order of your questions and the order of the
answer alternatives. Usually, random.sample() picks out a few random samples from a
collection. However, if you ask for as many samples as there are items in the sequence,
then you’re effectively randomly reordering the whole sequence:
Python >>>
Throughout this step, you’ve improved on your quiz application. It’s now time to take a
step back and consider the code itself. In the next section, you’ll reorganize the code so
that you keep it modular and ready for further development.
Currently, your code isn’t particularly organized. All your statements are fairly low level.
You’ll define functions to improve your code. A few of their advantages are the following:
Functions name higher-level operations that can help you get an overview of your
code.
Functions can be reused.
To see how the code will look after you’ve refactored it, click below and check out the
source_code_step_3 folder:
Get Source Code: Click here to get access to the source code that you’ll use to
build your quiz application.
Remove ads
Prepare Data
Many games and applications follow a common life cycle:
In your quiz application, you first read the available questions, then you ask each of the
questions, before finally reporting the final score. If you look back at your current code,
then you’ll see these three steps in the code. But the organization is still a bit hidden
within all the details.
You can make the main functionality clearer by encapsulating it in a function. You don’t
need to update your quiz.py file yet, but note that you can translate the previous
paragraph into code that looks like this:
Python
def run_quiz():
# Preprocess
questions = prepare_questions()
num_correct = 0
num_correct += ask_question(question)
# Postprocess
This code won’t run as it is. The functions prepare_questions() and ask_question()
haven’t been defined, and there are some other details missing. Still, run_quiz()
encapsulates the functionality of your application at a high level.
Writing down your application flow at a high level like this can be a great start to uncover
which functions are natural building blocks in your code. In the rest of this section, you’ll
fill in the missing details:
Implement prepare_questions().
Implement ask_question().
Revisit run_quiz().
You’re now going to make quite substantial changes to the code of your quiz application
as you’re refactoring it to use functions. Before doing so, it’s a good idea to make sure you
can revert to the current state, which you know works. You can do this either by saving a
copy of your code with a different filename or by making a commit if you’re using a
version control system.
Once you’ve safely stored your current code, start with a new quiz.py that only contains
your imports and global variables. You can copy these from your previous version:
Python
# quiz.py
import random
NUM_QUESTIONS_PER_QUIZ = 5
QUESTIONS = {
],
Remember that you’re only reorganizing your code. You’re not adding new functionality,
so you won’t need to import any new libraries.
Next, you’ll implement the necessary preprocessing. In this case, this means that you’ll
prepare the QUESTIONS data structure so that it’s ready to be used in your main loop. For
now, you’ll potentially limit the number of questions and make sure they’re listed in a
random order:
Python
# quiz.py
# ...
Ask Questions
Look back on your sketch for the run_quiz() function and remember that it contains
your main loop. For each question, you’ll call ask_question(). Your next task is to
implement that helper function.
These are a lot of small things to do in one function, and you could consider whether
there’s potential for further modularization. For example, items 3 to 6 in the list above are
all about interacting with the user, and you can pull them into yet another helper
function.
To achieve this modularization, add the following get_answer() helper function to your
source code:
Python
# quiz.py
# ...
print(f"{question}?")
return labeled_alternatives[answer_label]
This function accepts a question text and a list of alternatives. You then use the same
techniques as earlier to label the alternatives and ask the user to enter a valid label.
Finally, you return the user’s answer.
Python
# quiz.py
# ...
correct_answer = alternatives[0]
if answer == correct_answer:
print(" ⭐Correct! ⭐
")
return 1
else:
return 0
You first randomly reorder the answer alternatives using random.shuffle(), as you did
earlier. Next, you call get_answer(), which handles all details about getting an answer
from the user. You can therefore finish up ask_question() by checking the correctness of
the answer. Observe that you return 1 or 0, which indicates to the calling function
whether the answer was correct or not.
Note: You could replace the return values with Booleans. Instead of 1, you could
return True, and instead of 0, you could return False. This would work because
Python treats Booleans as integers in calculations:
Python >>>
In some cases, your code reads more naturally when you use True and False. In
this case, you’re counting correct answers, so it seems more intuitive to use
numbers.
You’re now ready to implement run_quiz() properly. One thing you’ve learned while
implementing prepare_questions() and ask_question() is which arguments you need
to pass on:
Python
# quiz.py
# ...
def run_quiz():
questions = prepare_questions(
QUESTIONS, num_questions=NUM_QUESTIONS_PER_QUIZ
num_correct = 0
print(f"\nQuestion {num}:")
As earlier, you use enumerate() to keep a counter that numbers the questions you ask.
You can increment num_correct based on the return value of ask_question(). Observe
that run_quiz() is your only function that directly interacts with QUESTIONS and
NUM_QUESTIONS_PER_QUIZ.
Your refactoring is now complete, except for one thing. If you run quiz.py now, then it’ll
seem like nothing happens. In fact, Python will read your global variables and define your
functions. However, you’re not calling any of those functions. You therefore need to add a
function call that starts your application:
Python
# quiz.py
# ...
if __name__ == "__main__":
run_quiz()
You call run_quiz() at the end of quiz.py, outside of any function. It’s good practice to
protect such a call to your main function with an if __name__ == "__main__" test. This
special incantation is a Python convention that means that run_quiz() is called when
you run quiz.py as a script, but it’s not called when you import quiz as a module.
That’s it! You’ve refactored your code into several functions. This will help you in keeping
track of the functionality of your application. It’ll also be useful in this tutorial, as you can
consider changes to individual functions instead of changing the whole script.
For the rest of the tutorial, you’ll see your full code listed in collapsible boxes like the one
below. Expand these to see the current state and get an overview of your full application:
Through this step, you’ve refactored your code to make it more convenient to work with.
You separated your commands into well-organized functions that you can continue to
develop. In the next step, you’ll take advantage of this by improving how you read
questions into your application.
Remove ads
Step 4: Separate Data Into Its Own File
You’ll continue your refactoring journey in this step. Your focus will now be how you
provide questions to your application.
So far, you’ve stored the questions directly in your source code in the QUESTIONS data
structure. It’s usually better to separate your data from your code. This separation can
make your code more readable, but more importantly, you can take advantage of
systems designed for handling data if it’s not hidden inside your code.
In this section, you’ll learn how to store your questions in a separate data file formatted
according to the TOML standard. Other options—that you won’t cover in this tutorial—are
storing the questions in a different file format like JSON or YAML, or storing them in a
database, either a traditional relational one or a NoSQL database.
To peek at how you’ll improve your code in this step, click below and go to the
source_code_step_4 directory:
Get Source Code: Click here to get access to the source code that you’ll use to
build your quiz application.
TOML supports several data types, including strings, integers, floating-point numbers,
Booleans, and dates. Additionally, data can be structured in arrays and tables, which are
similar to Python’s lists and dictionaries, respectively. TOML has been gaining popularity
over the last years, and the format is stable after version 1.0.0 of the format specification
was released in January 2021.
Create a new text file that you’ll call questions.toml, and add the following content:
TOML
# questions.toml
"Which version of Python is the first with TOML support built in" = [
While there are differences between TOML syntax and Python syntax, you’ll recognize
elements like using quotation marks (") for text and square brackets ([]) for lists of
elements.
To work with TOML files in Python, you need a library that parses them. In this tutorial,
you’ll use tomli. This will be the only package you use in this project that’s not part of
Python’s standard library.
Note: TOML support is added to Python’s standard library in Python 3.11. If you’re
already using Python 3.11, then you can skip the instructions below to create a
virtual environment and install tomli. Instead, you can immediately start coding by
replacing any mentions of tomli in your code with the compatible tomllib.
Later in this section, you’ll learn how to write code that can use tomllib if it’s
available and fall back to tomli if necessary.
Before installing tomli, you should create and activate a virtual environment:
Windows PowerShell
PS> venv\Scripts\Activate.ps1
Windows PowerShell
You can check that you have tomli available by parsing questions.toml, which you
created earlier. Open up your Python REPL and test the following code:
Python >>>
...
>>> questions
First, observe that questions is a regular Python dictionary that has the same form as
your QUESTIONS data structure that you’ve been using so far.
You can use tomli to parse TOML information in two different ways. In the example
above, you use tomli.load() to read TOML from an open file handle. Alternatively, you
could use tomli.loads() to read TOML from a text string.
Note: You need to open files in binary mode by using mode="rb" before passing
them to tomli.load(). This is so that tomli can make sure that the UTF-8 encoding
of the TOML file is correctly handled.
If you use tomli.loads(), then the string you pass in will be interpreted as UTF-8.
You can integrate the TOML file into your quiz application by updating the preamble of
your code, where you do your imports and define the global variables:
Python
# quiz.py
# ...
import pathlib
try:
import tomllib
except ModuleNotFoundError:
NUM_QUESTIONS_PER_QUIZ = 5
QUESTIONS = tomllib.loads(QUESTIONS_PATH.read_text())
# ...
Instead of doing a plain import tomli like you did earlier, you wrap your import in a try
… except statement that first tries to import tomllib. If that fails, then you import tomli
but rename it to tomllib. The effect of this is that you’ll use the Python 3.11 tomllib if
it’s available and fall back to tomli if it’s not.
You’re using pathlib to handle the path to questions.toml. Instead of hard-coding the
path to questions.toml you rely on the special __file__ variable. In practice, you’re
stating that it’s located in the same directory as your quiz.py file.
Finally, you use read_text() to read the TOML file as a text string and then loads() to
parse that string into a dictionary. As you saw in the previous example, loading the TOML
file results in the same data structure as you previously had for your questions. Once
you’ve made the changes to quiz.py, your quiz application should still function the
same, although the questions are defined in the TOML file instead of in your source code.
Go ahead and add a few more questions to your TOML file to confirm that it’s being used.
Remove ads
One notable feature of TOML is tables. These are named sections that map to nested
dictionaries in Python. Furthermore, you can use arrays of tables, which are represented
by lists of dictionaries in Python.
You can take advantage of these to be more explicit when defining your questions.
Consider the following TOML snippet:
TOML
[[questions]]
question = "Which version of Python is the first with TOML support built in"
answer = "3.11"
Regular tables start with a single-bracketed line like [questions]. You indicate an array
of tables by using double brackets, like above. You can parse the TOML with tomli:
Python >>>
... [[questions]]
... question = "Which version of Python is the first with TOML support built
... answer = "3.11"
... """
>>> tomli.loads(toml)
{'questions': [
'question': 'Which version of Python is the first with TOML support buil
'answer': '3.11',
]}
This results in a nested data structure, with an outer dictionary in which the questions
key points to a list of dictionaries. The inner dictionaries have the question, answer, and
alternatives keys.
This structure is a bit more complicated than what you’ve used so far. However, it’s also
more explicit, and you don’t need to rely on conventions such as the first answer
alternative representing the correct answer.
You’ll now convert your quiz application so that it takes advantage of this new data
structure for your questions. First, reformat your questions in questions.toml. You
should format them as follows:
TOML
# questions.toml
[[questions]]
question = "Which version of Python is the first with TOML support built in"
answer = "3.11"
[[questions]]
answer = "Array"
Each question is stored inside an individual questions table with key-value pairs for the
question text, the correct answer, and the answer alternatives.
Principally, you’ll need to make two changes to your application source code to use the
new format:
These changes touch on your main data structure, so they require several small code
changes throughout your code.
First, change how you read the questions from the TOML file:
Python
# quiz.py
# ...
NUM_QUESTIONS_PER_QUIZ = 5
def run_quiz():
questions = prepare_questions(
QUESTIONS_PATH, num_questions=NUM_QUESTIONS_PER_QUIZ
num_correct = 0
print(f"\nQuestion {num}:")
num_correct += ask_question(question)
questions = tomllib.loads(path.read_text())["questions"]
You change prepare_questions() to do the reading of the TOML file and pick out the
questions list. Additionally, you can simplify the main loop in run_quiz() since all
information about a question is contained in a dictionary. You don’t keep track of the
question text and alternatives separately.
Python
# quiz.py
# ...
def ask_question(question):
correct_answer = question["answer"]
if answer == correct_answer:
print(" ⭐ Correct! ⭐
")
return 1
else:
return 0
You now pick out the question text, the correct answer, and the answer alternatives
explicitly from the new question dictionary. One nice thing with this is that it’s more
readable than the earlier convention of assuming the first answer alternative to be the
correct answer.
You don’t need to make any changes in get_answer(), because that function already
dealt with question text and lists of alternatives in general. That hasn’t changed.
You can find the current, full source code of your application inside the collapsed
sections below:
Your new flexible format for defining questions gives you some options in adding more
functionality to your quiz application. You’ll dive into some of these in the next step.
Remove ads
Get Source Code: Click here to get access to the source code that you’ll use to
build your quiz application.
First, you need to consider how you can represent several correct answers in your
questions.toml data file. One advantage of the more explicit data structure that you
introduced in the previous step is that you can use an array to specify the correct answers
as well. Replace each answer key in your TOML file with an answers key that wraps each
correct answer within square brackets ([]).
Your questions file will then look something like the following:
TOML
# questions.toml
[[questions]]
answers = ["Array"]
[[questions]]
For old questions with only one correct answer, there will be only one answer listed in the
answers array. The last question above shows an example of a question with two correct
answer alternatives.
Once your data structure is updated, you’ll need to implement the feature in your code as
well. You don’t need to make any changes in run_quiz() or prepare_questions(). In
ask_question() you need to check that all correct answers are given, while in
get_answer(), you need to be able to read multiple answers from the user.
Start with the latter challenge. How can the user enter multiple answers, and how can
you validate that each one is valid? One possibility is to enter multiple answers as a
comma-separated string. You can then convert the string to a list as follows:
Python >>>
You could use .split(",") to split directly on commas. However, first replacing commas
with spaces and then splitting on the default whitespace adds some leniency with spaces
allowed around the commas. This will be a better experience for your users, as they can
write a,b, a, b, or even a b without commas, and your program should interpret it as
intended.
The test for valid answers becomes a bit more complicated, though. You therefore
replace the tight while loop with a more flexible one. In order to loop until you get a valid
answer, you initiate an infinite loop that you return out of once all tests are satisfied.
Rename get_answer() to get_answers() and update it as follows:
Python
# quiz.py
# ...
print(f"{question}?")
while True:
if len(answers) != num_choices:
continue
if any(
):
print(
continue
Before looking too closely at the details in the code, take the function for a test run:
Python >>>
>>> get_answers(
... )
a) one
b) two
c) three
d) four
['four', 'two']
Your function first checks that the answer includes the appropriate number of choices.
Then each one is checked to make sure it’s a valid choice. If any of these checks fail, then
a helpful message is printed to the user.
In the code, you also make some effort to handle the distinction between one and several
items when it comes to grammar. You use plural_s to modify text strings to include
plural s when needed.
Additionally, you convert the answers to a set to quickly ignore duplicate alternatives. An
answer string like "a, b, a" is interpreted as {"a", "b"}.
Finally, note that get_answers() returns a list of strings instead of the plain string
returned by get_answer().
Next, you adapt ask_question() to the possibility of multiple correct answers. Since
get_answers() already handles most of the complications, what’s left is to check all
answers instead of only one. Recall that question is a dictionary with all information
about a question, so you don’t need to pass alternatives any longer.
Because the order of the answers is irrelevant, you use set() when comparing the given
answers to the correct ones:
Python
# quiz.py
# ...
def ask_question(question):
correct_answers = question["answers"]
answers = get_answers(
question=question["question"],
alternatives=ordered_alternatives,
num_choices=len(correct_answers),
if set(answers) == set(correct_answers):
else:
You only score a point for the user if they find all the correct answers. Otherwise, you list
all correct answers. You can now run your Python quiz application again:
Shell
$ python quiz.py
Question 1:
a) python -m quiz
b) python quiz
c) python quiz.py
d) python -m quiz.py
⭐ Correct!
⭐
Question 2:
a) Array
b) Set
c) Sequence
d) List
Choice? e
Choice? c
- Array
Table of Contents
Project Overview
Prerequisites
Allowing multiple correct answers gives you more flexibility in which kinds of questions Step 1: Ask Questions
you can ask in your quizzes. Step 2: Make Your Application User-
Friendly
Step 3: Organize Your Code With
Functions
Step 4: Separate Data Into Its Own
File
Remove ads
→ Step 5: Expand Your Quiz
Functionality
Add Hints to Help the User Step 6: Support Several Quiz Topics
Conclusion
Sometimes when you’re asked a question, you need a bit of help to jog your memory. Next Steps
Giving the users the option of seeing a hint can make your quizzes more fun. In this
section, you’ll extend your application to include hints.
Mark as Completed
You can include hints in your questions.toml data file, for example by adding hint as an
optional key-value pair: Tweet
Share
Email
TOML
# questions.toml
[[questions]]
hint = "One option uses the filename, and the other uses the module name."
[[questions]]
alternatives = [
Each question in the TOML file is represented by a dictionary in Python. The new hint
fields show up as new keys in those dictionaries. One effect of this is that you don’t need
to change how you read the question data, even when you make small changes to your
data structure.
Instead, you adapt your code to take advantage of the new optional field. In
ask_question() you only need to make one small change:
Python
# quiz.py
# ...
def ask_question(question):
# ...
answers = get_answers(
question=question["question"],
alternatives=ordered_alternatives,
num_choices=len(correct_answers),
hint=question.get("hint"),
# ...
Again, you’ll make bigger changes to get_answers(). You’ll add the hint as one of the
answer alternatives, with a special question mark (?) label:
Python
# quiz.py
# ...
print(f"{question}?")
if hint:
labeled_alternatives["?"] = "Hint"
while True:
# Handle hints
print(f"\nHINT: {hint}")
continue
# ...
If a hint is provided, then it’s added to the end of labeled_alternatives. The user can
then use ? to see the hint printed to the screen. If you test your quiz application, then
you’ll now get a bit of friendly help:
Shell
$ python quiz.py
Question 1:
What's a PEP?
?) Hint
Choice? ?
Choice? c
⭐ Correct! ⭐
In the next section, you’ll add a similar feature. In addition to showing an optional hint
before the user answers a question, you’ll show an explanation after they’ve answered it.
# questions.toml
[[questions]]
alternatives = [
explanation = """
information to the Python community. PEPs are used to propose new featur
for the Python language, to collect community input on an issue, and to
"""
[[questions]]
answers = [
alternatives = [
hint = "They're parsed from your code and stored on the function object."
explanation = """
"""
TOML supports multiline strings by using triple quotes (""") in the same way as Python.
These are great for explanations that may span a few sentences.
The explanations will be printed to the screen after the user has answered a question. In
other words, the explanations aren’t part of the user interaction done in get_answers().
Instead, you’ll print them inside ask_question():
Python
# quiz.py
# ...
def ask_question(question):
correct_answers = question["answers"]
answers = get_answers(
question=question["question"],
alternatives=ordered_alternatives,
num_choices=len(correct_answers),
hint=question.get("hint"),
else:
if "explanation" in question:
print(f"\nEXPLANATION:\n{question['explanation']}")
Because you print the explanation after giving the user feedback on whether their answer
was correct or not, you can’t return inside the if … else block any longer. You therefore
move the return statement to the end of the function.
Your explanations look something like the following when you run your quiz application:
Shell
$ python quiz.py
Question 1:
?) Hint
EXPLANATION:
The improvements to your Python quiz application add up. Feel free to expand the
collapsed sections below to see the full source code with all your new features:
In the final step, you’ll add one more feature: support for several quiz topics within your
application.
Remove ads
The final version of your Python quiz application will look as follows:
More topics and new questions will keep your quiz application fresh. Click below and
navigate to the source_code_final directory to see how the source code will look after
you’ve added these:
Get Source Code: Click here to get access to the source code that you’ll use to
build your quiz application.
Sections in TOML files can be nested. You create nested tables by adding periods (.) in
the section headers. As an illustrative example, consider the following TOML document:
Python >>>
... [python]
... label = "Python"
...
... [python.version]
... """
>>> tomli.loads(toml)
You can reorganize questions.toml to include a section for each topic. In addition to the
nested questions arrays, you’ll add a label key that provides a name for each topic.
Update your data file to use the following format:
TOML
# questions.toml
[python]
label = "Python"
[[python.questions]]
answers = [
alternatives = [
hint = "They're parsed from your code and stored on the function object."
explanation = """
"""
[[python.questions]]
explanation = """
Python 2.0 and Python 3.0, were released in October 2000 and December
2008, respectively.
"""
[capitals]
label = "Capitals"
[[capitals.questions]]
answers = ["Oslo"]
hint = "Lars Onsager, Jens Stoltenberg, Trygve Lie, and Børge Ousland."
explanation = """
trading place. It became the capital of Norway in 1299. The city was
of the reigning king. The city was renamed back to Oslo in 1925.
"""
[[capitals.questions]]
answers = ["Austin"]
explanation = """
"""
Now, there are two topics included in the data file: Python and Capitals. Within each topic
section, the question tables are still structured the same as before. This means that the
only change you need to make is how you prepare the questions.
You start by reading and parsing questions.toml. Next, you pick out each topic and store
it in a new, temporary dictionary. You need to ask the user about which topic they’d like
to try. Luckily, you can reuse get_answers() to get input about this. Finally, you pick out
the questions belonging to the chosen topic and shuffle them up:
Python
# quiz.py
# ...
topic_info = tomllib.loads(path.read_text())
topics = {
topic_label = get_answers(
alternatives=sorted(topics),
)[0]
questions = topics[topic_label]
The data structure returned by prepare_questions() is still the same as before, so you
don’t need to make any changes to run_quiz(), ask_question(), or get_answers().
When these kinds of updates only require you to edit one or a few functions, that’s a good
sign indicating that your code is well-structured, with good abstractions.
Run your Python quiz application. You’ll be greeted by the new topic prompt:
Shell
$ python quiz.py
a) Capitals
b) Python
Choice? a
Question 1:
a) Reykjavik
b) Helsinki
c) Stockholm
d) Copenhagen
e) Oslo
?) Hint
Choice? ?
HINT: Lars Onsager, Jens Stoltenberg, Trygve Lie, and Børge Ousland.
Choice? e
⭐ Correct! ⭐
EXPLANATION:
trading place. It became the capital of Norway in 1299. The city was
of the reigning king. The city was renamed back to Oslo in 1925.
This ends the guided part of this journey. You’ve created a powerful Python quiz
application in the terminal. You can see the complete source code as well as a list of
questions by expanding the boxes below:
Python
quiz.py
mport pathlib
mport random
ry:
import tomllib
xcept ModuleNotFoundError:
UM_QUESTIONS_PER_QUIZ = 5
ef run_quiz():
questions = prepare_questions(
QUESTIONS_PATH, num_questions=NUM_QUESTIONS_PER_QUIZ
num_correct = 0
print(f"\nQuestion {num}:")
num_correct += ask_question(question)
ef prepare_questions(path, num_questions):
topic_info = tomllib.loads(path.read_text())
topics = {
topic_label = get_answers(
alternatives=sorted(topics),
)[0]
questions = topics[topic_label]
ef ask_question(question):
correct_answers = question["answers"]
answers = get_answers(
question=question["question"],
alternatives=ordered_alternatives,
num_choices=len(correct_answers),
hint=question.get("hint"),
else:
if "explanation" in question:
print(f"\nEXPLANATION:\n{question['explanation']}")
print(f"{question}?")
if hint:
labeled_alternatives["?"] = "Hint"
while True:
# Handle hints
print(f"\nHINT: {hint}")
continue
if len(answers) != num_choices:
continue
if any(
):
print(
continue
f __name__ == "__main__":
run_quiz()
You can also access the source code and the questions file by clicking below:
Get Source Code: Click here to get access to the source code that you’ll use to
build your quiz application.
You’ll find the final version of the application in the directory source_code_final.
Conclusion
Good job! You’ve created a flexible and useful quiz application with Python. Along the
way, you’ve learned how you can start with a basic script and build it out to a more
complex program.
Now, go have some fun with your quiz application. Add some questions on your own, and
challenge your friends. Share your best questions and quiz topics in the comments
below!
Next Steps
As you’ve followed along in this tutorial, you’ve created a well-featured quiz application.
However, there’s still ample opportunity to improve on the project.
Quiz creator: Add a separate application that interactively asks for questions and
answers and stores them in the proper TOML format.
Store data in a database: Replace the TOML data file with a proper database.
Question Hub: Create a central questions database online that your application can
connect to.
Multiuser challenges: Allow different users to challenge each other in a trivia
competition.
You can also reuse the logic in this quiz application but change the front-end
presentation layer. Maybe you can convert the project to a web application. Feel free to
share your improvements in the comments below.
Mark as Completed
🐍 Python Tricks 💌
Get a short & sweet Python Trick delivered to your inbox every couple of
days. No spam ever. Unsubscribe any time. Curated by the Real Python
team.
Email Address
Geir Arne is an avid Pythonista and a member of the Real Python tutorial team.
Each tutorial at Real Python is created by a team of developers so that it meets our high quality standards. The team members who
worked on this tutorial are:
Tweet
Share
Email
Real Python Comment Policy: The most useful comments are those written
with the goal of learning from or helping out other readers—after reading the
whole article and all the earlier comments. Complaints and insults generally
won’t make the cut here.
What’s your #1 takeaway or favorite thing you learned? How are you going to put
your newfound skills to use? Leave a comment below and let us know.
Keep Learning
Remove ads
❤️ Happy Pythoning!