Как написать опрос на python

Пример на ввод и вывод информации и условия в python Диалог с компьютером на питон

В данном примере мы разберем программу на python  с вводом и выводом данных, условиями Программа на питон будет проводить опрос интересов пользователя и анализировать  их. Разберем структуру программы опроса на питон
k=0 
переменная для подсчета совпавших ответов
a1=3
a2=1
a3=3
a4=4

номера правильных ответов
n=input(«Как вас зовут?»)
Запрашиваем имя пользователя через операцию ввода информации в питон с клавиатуры
print(«Здравствуйте»,n)
Вывод информации на экран  в python в виде приветствия
a=int(input(«Ваш любимый жанр фильмов? 1- боевики 2-детективы 3-фантастика»))
Ввод ответа на вопрос ответ записывается в переменную a при вводе ответ преобразуется в целую переменную с помощью операции int()
if (a==a1):
    k=k+1

Анализируем  совпадения ответа с помощью условного оператора if в питон. Если ответ совпал увеличиваем число совпавших ответов Аналогично с другими вопросами опроса. В конце программы проводим анализ числа совпадений и вывод
if (k>=2):
    print(n,»мы с вами подружимся»)
if (k<2):
    print(n+» мы с вами очень разные»)

Полный текст программы опроса, теста, диалога на python
k=0
a1=3
a2=1
a3=3
a4=4
n=input(«Как вас зовут?»)
print(«Здравствуйте»,n)
a=int(input(«Ваш любимый жанр фильмов? 1- боевики 2-детективы 3-фантастика»))
if (a==a1):
    k=k+1
a=int(input(«Ваша любимая музыка? 1- классика 2-рэп 3-поп»))
if (a==a2):
    k=k+1
a=int(input(«Ваш любимый цвет? 1- красный 2-желтый 3-зеленый»))
if (a==a3):
    k=k+1
a=int(input(«Ваше любимый время года? 1- осень 2-зима 3-весна 4- лето»))
if (a==a4):
    k=k+1
if (k>=2):
    print(n,»мы с вами подружимся»)
if (k<2):
    print(n+» мы с вами очень разные»)

подружимся»)
if (k<2):
    print(n+» мы с вами очень разные»)

Вернуться к содержанию

Полезно почитать по теме условия в Python пример
Решение линейного уравнения на Python

Поделиться:

Комментарии ()

Нет комментариев. Ваш будет первым!

Опросы v2.0

Введение

Добро пожаловать в 2020! В последний раз мы рассматривали нововведения Bot API аж в далёком 2017 году, когда появилось удаление сообщений и ограничения в чатах. С тех пор вышло много чего интересного и, возможно, о чём-то стоит выпустить отдельные уроки.

А сегодня мы познакомимся с опросами 2.0, точнее, с новой сущностью: викторинами (quiz). Викторина – это именно то, что вы и предположили; тест с одним правильными вариантом ответа и ещё N неправильными.

Поставим себе задачу сделать бота, который умеет:

  1. принимать от пользователя только викторины;
  2. запоминать их содержимое и записывать к себе в память;
  3. предлагать викторины в инлайн-запросе и отправляет их в группу;
  4. получать новые ответы и сохранять ID правильно ответивших;
  5. останавливать викторину после двух правильных ответов и поздравлять победителей.

Задач много, придётся вспомнить, что такое колбэки, инлайн-режим и классы. Но и это не самое главное…

Пятиминутка ненависти к telebot или Привет, aiogram!

Как вы знаете, во всех предыдущих уроках использовалась библиотека pyTelegramBotAPI, именуемая в коде telebot. В 2015-2017 годах, возможно, она ещё была актуальна, но прогресс не стоит на месте. А telebot, увы, стоит. Кривая реализация поллинга, проблемный next_step_handler, медленная поддержка новых версий Bot API и т.д.

В течение 2019 года я постепенно переносил своих ботов на другой фреймворк, который по многим пунктам превосходит pyTelegramBotAPI, и имя ему – aiogram. «Почему?», спросит меня уважаемый читатель. Что ж, приведу следующие аргументы:

  • это полноценный фреймворк, т.е. позволяет сделать больше полезных вещей;
  • асинхронный, что делает его быстрее в некоторых задачах;
  • поддерживается Python 3.7+ и выше, что сподвигнет обновить свой старенький интерпретатор и использовать новые возможности языка;
  • множество встроенных «помощников» (синтаксический «сахар»), улучшающих читабельность кода;
  • оперативные обновления (поддержка новых опросов появилась в тот же день, что и в самом Bot API);
  • русскоязычный чат поддержки и обсуждений, где сидит, в том числе, и сам разработчик фреймворка;
  • мой любимый пункт: нормально работающий поллинг.

Прокомментирую последний пункт: в настоящий момент почти все мои боты работают на aiogram-ном поллинге и не падают ежедневно, как в случае с pyTelegramBotAPI.

Введение получилось очень большим, поэтому давайте уже перейдём к делу.

Плацдарм для бота

Напишем элементарного эхо-бота на aiogram с поллингом, чтобы бегло ознакомиться с фреймворком. Прежде всего, добавим нужные импорты (предполагается, что мы используем Virtual Environment, подробнее о нём – в уроке №0):

#!venv/bin/python
import logging
from aiogram import Bot, Dispatcher, executor, types
logging.basicConfig(level=logging.INFO)

Теперь создадим объект бота. А за хэндлеры здесь отвечает специальный Диспетчер:

bot = Bot(token="12345678:AABcdeFGhIJkXyZ")
dp = Dispatcher(bot)

Далее напишем простейший хэндлер, повторяющий текстовые сообщения:

@dp.message_handler()
async def echo(message: types.Message):
    await message.reply(message.text)

Началась магия.
Во-первых, как я написал чуть выше, за хэндлеры отвечает диспетчер (dp).
Во-вторых, подхэндлерные функции в aiogram асинхронные (async def), вызовы Bot API тоже асинхронные, поэтому необходимо использовать ключевое слово await.
В-третьих, вместо bot.send_message можно для удобства использовать message.reply( ) без указания chat_id и message.id, чтобы бот сделал «ответ» (reply), либо аналог message.answer( ), чтобы просто отправить в тот же чат, не создавая «ответ». Само выражение в хэндлере пустое, т.к. нас устроят любые текстовые сообщения.

Наконец, запуск!

if __name__ == "__main__":
    executor.start_polling(dp, skip_updates=True)

Параметр skip_updates=True позволяет пропустить накопившиеся входящие сообщения, если они нам не важны.
Запускаем код, убеждаемся в его работоспособности, после чего удаляем хэндлер вместе с функцией echo, нам они больше не понадобятся, в отличие от остального кода.

Запрашиваем викторину у пользователя

В BotAPI 4.6 появилась новая кнопка для обычной (не инлайн) клавиатуры с типом KeyboardButtonPollType. При нажатии на неё в приложении Telegram появляется окно для создания опроса. В самой кнопке можно выставить ограничение по типу создаваемого объекта: опрос, викторина или что угодно. Опросы нас пока не интересуют, поэтому напишем обработчик команды /start, выводящий приветственное сообщение и обычную клавиатуру с двумя кнопками: “Создать викторину” и “Отмена”, причём вторая отправляет ReplyKeyboardRemove, удаляя первую клавиатуру.

# Хэндлер на команду /start
@dp.message_handler(commands=["start"])
async def cmd_start(message: types.Message):
    poll_keyboard = types.ReplyKeyboardMarkup(resize_keyboard=True)
    poll_keyboard.add(types.KeyboardButton(text="Создать викторину",
                                           request_poll=types.KeyboardButtonPollType(type=types.PollType.QUIZ)))
    poll_keyboard.add(types.KeyboardButton(text="Отмена"))
    await message.answer("Нажмите на кнопку ниже и создайте викторину!", reply_markup=poll_keyboard)

# Хэндлер на текстовое сообщение с текстом “Отмена”
@dp.message_handler(lambda message: message.text == "Отмена")
async def action_cancel(message: types.Message):
    remove_keyboard = types.ReplyKeyboardRemove()
    await message.answer("Действие отменено. Введите /start, чтобы начать заново.", reply_markup=remove_keyboard)


Клавиатура с кнопками

Сохраняем и предлагаем

В 11-м уроке я использовал библиотеку Vedis для сохранения состояний в файле, чтобы те не сбрасывались после перезагрузки бота. В этот раз мы будем сохранять всё в памяти, а выбор постоянного хранилища останется за читателем, чтобы не навязывать то или иное решение. Разумеется, данные в памяти сотрутся при остановке бота, но для примера так даже лучше.

Наше хранилище будет основано на стандартных питоновских словарях (dict), причём их будет два: первый словарь содержит пары (“id пользователя”, “массив сохранённых викторин”), а второй — пары (“id викторины”, “id автора викторины”). Зачем два словаря? В дальнейшем нам нужно будет по идентификатору викторины получать некоторую информацию о ней. Необходимые нам сведения лежат в первом словаре, но в виде значений, а не ключей. Поэтому нам пришлось бы проходиться по всем возможным парам ключ-значение, чтобы найти нужную викторину.

Для ускорения поиска мы заведём второй словарь, чтобы по идентификатору викторины сразу же найти идентификатор её автора, который, в свою очередь, является ключом в первом словаре. А дальше проход по небольшому массиву и вуаля! Наши данные получены. На словах звучит сложно, но на практике реализуется довольно быстро и с минимальной избыточностью. Если придумаете решение лучше — пишите, буду рад исправить текст.

Помимо определения викторины, нам нужно хранить некоторую дополнительную информацию. Поэтому давайте создадим файл quizzer.py, опишем наш класс Quiz со всеми нужными полями в конструкторе класса (обратите внимание, в конструктор передаются не все поля, т.к. часть из них будет заполнена позднее):

from typing import List

class Quiz:
    type: str = "quiz"

    def __init__(self, quiz_id, question, options, correct_option_id, owner_id):
        # Используем подсказки типов, чтобы было проще ориентироваться.
        self.quiz_id: str = quiz_id   # ID викторины. Изменится после отправки от имени бота
        self.question: str = question  # Текст вопроса
        self.options: List[str] = [*options] # "Распакованное" содержимое массива m_options в массив options
        self.correct_option_id: int = correct_option_id  # ID правильного ответа
        self.owner: int = owner_id  # Владелец опроса
        self.winners: List[int] = []  # Список победителей
        self.chat_id: int = 0  # Чат, в котором опубликована викторина
        self.message_id: int = 0  # Сообщение с викториной (для закрытия)

Если вы раньше не сталкивались с подсказками типов (type hints), код вида “chat_id: int = 0” может ввести в замешательство. Здесь chat_id — это имя переменной, далее через двоеточие int — её тип (число), а дальше инициализация числом 0. Python по-прежнему является языком с динамической типизацией, отсюда и название “подсказка типа”. В реальности это влияет только на восприятие кода и предупреждения в полноценных IDE типа PyCharm. Никто не мешает вам написать quiz_id: int = "чемодан", но зачем так делать?
Вернёмся в наш основной файл (я его далее буду называть bot.py) и импортируем наш класс: from quizzer import Quiz. Также добавим в начале файла под определением бота два пустых словаря:

quizzes_database = {}  # здесь хранится информация о викторинах
quizzes_owners = {}  # здесь хранятся пары "id викторины <—> id её создателя"

Теперь будем отлавливать викторины, приходящие в бота. Как только прилетает что-то, похожее на неё, извлекаем информацию и создаём две записи. В первом словаре храним параметры викторины, чтобы потом её воспроизвести, а во втором просто создаём пару викторина-создатель. Идентификаторы, составляющие ключ словаря, конвертируем в строки методом str():

Раз уж мы сохраняем викторины, давайте теперь позволим пользователям их отправлять, причём через инлайн-режим. Есть одна загвоздка: в BotAPI через инлайн-режим нельзя напрямую отправлять опросы (нет объекта InlineQueryResultPoll), поэтому придётся доставать костыли. Будем возвращать обычное сообщение с URL-кнопкой вида https://t.me/нашбот?startgroup=id_викторины. Параметры startgroup и start — это т.н. “глубокие ссылки” (Deep Linking). Когда пользователь нажмёт на кнопку, он перейдёт по указанной выше ссылке, что, в свою очередь, благодаря параметру startgroup перекинет его к выбору группы, а затем, уже после подтверждения выбора, бот будет добавлен в группу с вызовом команды /start id_викторины.

Начнём разбираться с инлайн-режимом (не забудьте включить его у @BotFather). Когда пользователь вызывает нашего бота через инлайн, показываем все созданные им викторины, плюс кнопку “Создать новую”. Если ничего нет, то только кнопку.

Очень важно выставить флаг is_personal равным True (ответ на запрос будет уникален для каждого Telegram ID) и указать небольшое значение параметра cache_time, чтобы кэш инлайн-ответов оперативно обновлялся по мере появления новых викторин.
Теперь при вызове бота через инлайн мы увидим наши сохранённые викторины, а при выборе одной из них — сообщение с кнопкой, по нажатию на которую нам предложат выбрать группу для отправки сообщения. Как только группа будет выбрана, в неё будет автоматически добавлен бот с сообщением вида /start@имя_бота. Но ничего не происходит! Сейчас разберёмся.

Отправляем викторину и получаем ответы

Помните наш простой обработчик команды /start, возвращающий сообщение с кнопкой? Настало время переписать этот хэндлер. Первым делом, будем проверять, куда отправлено сообщение – в диалог с ботом или нет. Если в диалог, то всё остаётся по-прежнему: приветствие (на этот раз укажем, что викторина принудительно будет сделана неанонимной) и кнопка для создания викторины.

А вот если сообщение отправлено в группу, то применяем следующую логику: проверяем количество “слов” в сообщении. Одно всегда есть (команда /start), но может быть и второе, невидимое в интерфейсе приложения Telegram – параметр, переданный в качестве параметра startgroup, в нашем случае это ID викторины. Если второго слова нет (количество слов = 1), то показываем сообщение с предложением перейти в личку к боту с принудительным показом кнопки /start.

В случае, если второе слово есть, то считаем его идентификатором и пробуем отправить викторину в ту же группу. При этом мы, по сути, воспроизводим её [викторину] заново, просто от своего имени: повторяем вопрос, варианты ответов и отключаем анонимный режим, т.к. нам нужно знать, кто победитель.

Очень важный момент: при отправке викторины, в объекте Message будет записан уже новый её идентификатор, который нужно подставить в наши словари. Далее по этому новому ID мы будем смотреть и считать ответы. Побочным эффектом такого подхода будет возможность использования конкретной викторины лишь однажды и в одном чате, если отправить сообщение из инлайна в другой чат, то зашитый в ссылке инлайн-кнопки ID будет недействительным.

Далее необходимо научиться как-то обрабатывать новые ответы. В свежем обновлении API добавилось два новых типа обновлений (updates, т.е. входящие события): PollAnswer и просто Poll. Первый срабатывает при получении новых ответов в викторинах и опросах, в последнем случае ещё и при отзыве голоса (массив голосов от пользователя будет пустой). Второй срабатывает при изменении состояния опроса в целом, т.е. не только при получении новых ответов/голосов, но и при смене состояния “открыт/закрыт” и др. Опять-таки, в обучающих целях мы задействуем хэндлеры на оба типа событий.

Начнём с PollAnswer. Когда прилетает событие с новым ответом на викторину, прежде всего достаём её ID, по ней ищем автора во втором словаре. Если находим, то гуляем по всем викторинам этого пользователя и ищем совпадение по ID самой викторины, т.е. в точности обратное действие, только уже в первом словаре. Когда обнаружится нужная викторина, то проверяем, верный ответ или нет (сравниваем с correct_option_id), и если да, то записываем ID пользователя в список победителей. Если количество победителей при этом достигает двух, то останавливаем викторину.

Остановка викторины (метод stop_poll( )) вызовет срабатывание хэндлера на тип обновлений Poll с условием is_closed is True. Снова извлекаем нужный нам экземпляр класса Quiz, вытаскиваем ID победителей и вызываем метод get_chat_member, после чего, используя aiogram-ный вспомогательный метод get_mention, формируем ссылку на каждого из победителей в HTML-разметке и создаём поздравительное сообщение. Викторины у нас одноразовые, поэтому подчищаем за собой словари, дабы не раздувать объекты в памяти.

Код готов. Закинем викторину в группу и попросим друзей правильно ответить, а сами ответим неправильно.
После первого правильного ответа:


2 ответа, только один правильный

После второго правильного ответа:


3 ответа, 2 правильных, опрос закрыт

На этом всё! Если у вас возникли вопросы, не стесняйтесь задавать их в нашем чатике, а если вы нашли ошибку/опечатку, либо есть чем дополнить материал, то добро пожаловать на GitHub (ну, или всё так же в чате). Полный код урока можно найти здесь.

← Урок №12
Урок №14 →

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:

When was the first known use of the word quiz?

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.

In this tutorial, you’ll learn how to:

  • Interact with the user in the terminal
  • Improve the usability of your application
  • Refactor your application to continuously improve it
  • Store data in dedicated data files

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:

Whether you’re an eccentric person or not, read on to learn how to create your own quiz.

Demo: Your Python Quiz Application

In this step-by-step project, you’ll build a terminal application that can quiz you and your friends on a range of topics:

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.

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:

  1. Create a basic application that can ask multiple-choice questions.
  2. Make the app more user-friendly by improving how it looks and how it handles user errors.
  3. Refactor the code to use functions.
  4. Separate question data from source code by storing questions in a dedicated data file.
  5. Expand the app to handle multiple correct answers, give hints, and provide explanations.
  6. Add interest by supporting different quiz topics to choose from.

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:

  • Reading input from the user at the terminal
  • Organizing data in structures like lists, tuples, and dictionaries
  • Using if statements to check different conditions
  • Repeating actions with for and while loops
  • Encapsulating code with functions

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.

Step 1: Ask Questions

In this step, you’ll learn how to create a program that can ask questions and check answers. This will be the foundation of your quiz application, which you’ll improve upon in the rest of the tutorial. At the end of this step, your program will look like this:

Your program will be able to ask questions and check answers. This version includes the basic functionality that you need, but you’ll add more functionality in later steps. If you prefer, then you can download the source code as it’ll look when you’re done with this step by clicking the link below and entering the source_code_step_1 directory:

Get User Information With input()

One of Python’s built-in functions is input(). You can use it to get information from the user. For a first example, run the following in a Python REPL:

>>>

>>> name = input("What's your name? ")
What's your name? Geir Arne

>>> 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:

>>>

>>> answer = input("When was the first known use of the word 'quiz'? ")
When was the first known use of the word 'quiz'? 1781

>>> answer == 1781
False

>>> answer == "1781"
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:

# quiz.py

answer = input("When was the first known use of the word 'quiz'? ")
if answer == "1781":
    print("Correct!")
else:
    print(f"The answer is '1781', not {answer!r}")

This code is very similar to what you experimented with in the REPL above. You can run your application to check your knowledge:

$ python quiz.py
When was the first known use of the word 'quiz'? 1871
The answer is '1781', not '1871'

If you happen to give the wrong answer, then you’ll be gently corrected so that you’ll hopefully do better next time.

A quiz with only one question isn’t very exciting! You can ask another question by repeating your code:

# quiz.py

answer = input("When was the first known use of the word 'quiz'? ")
if answer == "1781":
    print("Correct!")
else:
    print(f"The answer is '1781', not {answer!r}")

answer = input("Which built-in function can get information from the user? ")
if answer == "input":
    print("Correct!")
else:
    print(f"The answer is 'input', not {answer!r}")

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:

$ 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
The answer is 'input', not '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.

Use Lists and Tuples to Avoid Repetitive Code

Python provides several flexible and powerful data structures. You can usually replace repeated code with a tuple, a list, or a dictionary in combination with a for loop or a while loop.

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.

For now, choose a fairly simple data structure:

  • A list will hold several question elements.
  • Each question element will be a two-tuple consisting of the question text and the answer.

You can then store your questions as follows:

[
    ("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:

# 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")
]

for question, correct_answer in QUESTIONS:
    answer = input(f"{question}? ")
    if answer == correct_answer:
        print("Correct!")
    else:
        print(f"The answer is {correct_answer!r}, not {answer!r}")

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.

Next, you’ll make your quiz application easier to use by adding answer alternatives for each question.

Provide Multiple Choices

Using input() is a great way to read input from your user. However, the way you’re currently using it can end up being frustrating. For example, someone may answer one of your questions like this:

Which built-in function can get information from the user? input()
The answer is 'input', not '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:

  - get
  - input
  - print
  - 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:

  1. The question text
  2. The correct answer
  3. Answer alternatives

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.

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:

# quiz.py

QUESTIONS = {
    "When was the first known use of the word 'quiz'": [
        "1781", "1771", "1871", "1881"
    ],
    "Which built-in function can get information from the user": [
        "input", "get", "print", "write"
    ],
    "Which keyword do you use to loop over a given list of elements": [
        "for", "while", "each", "loop"
    ],
    "What's the purpose of the built-in zip() function": [
        "To iterate over two or more sequences at the same time",
        "To combine several strings into one",
        "To compress several files into one archive",
        "To get information from the user",
    ],
}

for question, alternatives in QUESTIONS.items():
    correct_answer = alternatives[0]
    for alternative in sorted(alternatives):
        print(f"  - {alternative}")

    answer = input(f"{question}? ")
    if answer == correct_answer:
        print("Correct!")
    else:
        print(f"The answer is {correct_answer!r}, not {answer!r}")

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:

$ python quiz.py
  - 1771
  - 1781
  - 1871
  - 1881
When was the first known use of the word 'quiz'? 1781
Correct!

...

  - To combine several strings into one
  - To compress several files into one archive
  - To get information from the user
  - To iterate over two or more sequences at the same time
What's the purpose of the built-in zip() function?
    To itertate over two or more sequences at the same time
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:

# quiz.py

QUESTIONS = {
    "Which keyword do you use to loop over a given list of elements": [
        "for", "while", "each", "loop"
    ],
    "What's the purpose of the built-in zip() function": [
        "To iterate over two or more sequences at the same time",
        "To combine several strings into one",
        "To compress several files into one archive",
        "To get information from the user",
    ],
    "What's the name of Python's sorting algorithm": [
        "Timsort", "Quicksort", "Merge sort", "Bubble sort"
    ],
}

for question, alternatives in QUESTIONS.items():
    correct_answer = alternatives[0]
    sorted_alternatives = sorted(alternatives)
    for label, alternative in enumerate(sorted_alternatives):
        print(f"  {label}) {alternative}")

    answer_label = int(input(f"{question}? "))
    answer = sorted_alternatives[answer_label]
    if answer == correct_answer:
        print("Correct!")
    else:
        print(f"The answer is {correct_answer!r}, not {answer!r}")

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.

Now, it’s more convenient to answer the questions:

$ python quiz.py
  0) each
  1) for
  2) loop
  3) while
Which keyword do you use to loop over a given list of elements? 2
The answer is 'for', not 'loop'
  0) To combine several strings into one
  1) To compress several files into one archive
  2) To get information from the user
  3) To iterate over two or more sequences at the same time
What's the purpose of the built-in zip() function? 3
Correct!
  0) Bubble sort
  1) Merge sort
  2) Quicksort
  3) Timsort
What's the name of Python's sorting algorithm? 3
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.

Step 2: Make Your Application User-Friendly

In this second step, you’ll improve on your quiz application to make it easier to use. In particular, you’ll improve the following:

  • How the application looks and feels
  • How you summarize the user’s results
  • What happens if your user enters a nonexistent alternative
  • Which order you present the questions and alternatives in

At the end of this step, your application will work as follows:

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:

Format the Output More Nicely

Look back at how your quiz application is currently presented. It’s not very attractive. There are no blank lines that tell you where a new question starts, and the alternatives are listed above the question, which is a bit confusing. Furthermore, the numbering of the different choices start at 0 instead of 1, which would be more natural.

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:

# quiz.py

from string import ascii_lowercase

QUESTIONS = {
    "What's the purpose of the built-in zip() function": [
        "To iterate over two or more sequences at the same time",
        "To combine several strings into one",
        "To compress several files into one archive",
        "To get information from the user",
    ],
    "What's the name of Python's sorting algorithm": [
        "Timsort", "Quicksort", "Merge sort", "Bubble sort"
    ],
    "What does dict.get(key) return if key isn't found in dict": [
        "None", "key", "True", "False",
    ]
}

for num, (question, alternatives) in enumerate(QUESTIONS.items(), start=1):
    print(f"nQuestion {num}:")
    print(f"{question}?")
    correct_answer = alternatives[0]
    labeled_alternatives = dict(zip(ascii_lowercase, sorted(alternatives)))
    for label, alternative in labeled_alternatives.items():
        print(f"  {label}) {alternative}")

    answer_label = input("nChoice? ")
    answer = labeled_alternatives.get(answer_label)
    if answer == correct_answer:
        print("⭐ Correct! ⭐")
    else:
        print(f"The answer is {correct_answer!r}, not {answer!r}")

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:

>>>

>>> import string
>>> dict(zip(string.ascii_lowercase, ["1771", "1781", "1871", "1881"]))
{'a': '1771', 'b': '1781', 'c': '1871', 'd': '1881'}

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:

$ python quiz.py

Question 1:
What's the purpose of the built-in zip() function?
  a) To combine several strings into one
  b) To compress several files into one archive
  c) To get information from the user
  d) To iterate over two or more sequences at the same time

Choice? d
⭐ Correct! ⭐

Question 2:
What's the name of Python's sorting algorithm?
  a) Bubble sort
  b) Merge sort
  c) Quicksort
  d) Timsort

Choice? c
The answer is 'Timsort', not 'Quicksort'

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:

# quiz.py

from string import ascii_lowercase

QUESTIONS = {
    "What does dict.get(key) return if key isn't found in dict": [
        "None", "key", "True", "False",
    ],
    "How do you iterate over both indices and elements in an iterable": [
        "enumerate(iterable)",
        "enumerate(iterable, start=1)",
        "range(iterable)",
        "range(iterable, start=1)",
    ],
}

num_correct = 0
for num, (question, alternatives) in enumerate(QUESTIONS.items(), start=1):
    print(f"nQuestion {num}:")
    print(f"{question}?")
    correct_answer = alternatives[0]
    labeled_alternatives = dict(zip(ascii_lowercase, sorted(alternatives)))
    for label, alternative in labeled_alternatives.items():
        print(f"  {label}) {alternative}")

    answer_label = input("nChoice? ")
    answer = labeled_alternatives.get(answer_label)
    if answer == correct_answer:
        num_correct += 1
        print("⭐ Correct! ⭐")
    else:
        print(f"The answer is {correct_answer!r}, not {answer!r}")

print(f"nYou got {num_correct} correct out of {num} questions")

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.

Handle User Errors

So far, you haven’t worried too much about what happens if the user enters an answer that’s not valid. In the different versions of your app, this oversight could result in the program raising an error or—less dramatically—registering a user’s invalid answer as wrong.

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:

>>>

>>> while (text := input()) != "quit":
...     print(f"Echo: {text}")
...
Hello!
Echo: Hello!
Walrus ...
Echo: 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:

# quiz.py

from string import ascii_lowercase

QUESTIONS = {
    "How do you iterate over both indices and elements in an iterable": [
        "enumerate(iterable)",
        "enumerate(iterable, start=1)",
        "range(iterable)",
        "range(iterable, start=1)",
    ],
    "What's the official name of the := operator": [
        "Assignment expression",
        "Named expression",
        "Walrus operator",
        "Colon equals operator",
    ],
}

num_correct = 0
for num, (question, alternatives) in enumerate(QUESTIONS.items(), start=1):
    print(f"nQuestion {num}:")
    print(f"{question}?")
    correct_answer = alternatives[0]
    labeled_alternatives = dict(zip(ascii_lowercase, sorted(alternatives)))
    for label, alternative in labeled_alternatives.items():
        print(f"  {label}) {alternative}")

    while (answer_label := input("nChoice? ")) not in labeled_alternatives:
        print(f"Please answer one of {', '.join(labeled_alternatives)}")

    answer = labeled_alternatives[answer_label]
    if answer == correct_answer:
        num_correct += 1
        print("⭐ Correct! ⭐")
    else:
        print(f"The answer is {correct_answer!r}, not {answer!r}")

print(f"nYou got {num_correct} correct out of {num} questions")

If you enter an invalid choice at the prompt, then you’ll be reminded about your valid choices:

$ python quiz.py

Question 1:
How do you iterate over both indices and elements in an iterable?
  a) enumerate(iterable)
  b) enumerate(iterable, start=1)
  c) range(iterable)
  d) range(iterable, start=1)

Choice? e
Please answer one of a, b, c, d

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.

Add Variety to Your Quiz

Currently, when you run your quiz application, you’re always asking the questions in the same order as they’re listed in your source code. Additionally, the answer alternatives for a given question also come in a fixed order that never changes.

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:

# quiz.py

import random
from string import ascii_lowercase

NUM_QUESTIONS_PER_QUIZ = 5
QUESTIONS = {
    "What's the official name of the := operator": [
        "Assignment expression",
        "Named expression",
        "Walrus operator",
        "Colon equals operator",
    ],
    "What's one effect of calling random.seed(42)": [
        "The random numbers are reproducible.",
        "The random numbers are more random.",
        "The computer clock is reset.",
        "The first random number is always 42.",
    ]
}

num_questions = min(NUM_QUESTIONS_PER_QUIZ, len(QUESTIONS))
questions = random.sample(list(QUESTIONS.items()), k=num_questions)

num_correct = 0
for num, (question, alternatives) in enumerate(questions, start=1):
    print(f"nQuestion {num}:")
    print(f"{question}?")
    correct_answer = alternatives[0]
    labeled_alternatives = dict(
        zip(ascii_lowercase, random.sample(alternatives, k=len(alternatives)))
    )
    for label, alternative in labeled_alternatives.items():
        print(f"  {label}) {alternative}")

    while (answer_label := input("nChoice? ")) not in labeled_alternatives:
        print(f"Please answer one of {', '.join(labeled_alternatives)}")

    answer = labeled_alternatives[answer_label]
    if answer == correct_answer:
        num_correct += 1
        print("⭐ Correct! ⭐")
    else:
        print(f"The answer is {correct_answer!r}, not {answer!r}")

print(f"nYou got {num_correct} correct out of {num} questions")

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:

>>>

>>> import random
>>> random.sample(["one", "two", "three"], k=3)
['two', 'three', 'one']

Additionally, you cap the number of questions in the quiz to NUM_QUESTIONS_PER_QUIZ which is initially set to five. If you include more than five questions in your application, then this also adds some variety as to which questions get asked in addition to the order in which they’re asked.

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.

Step 3: Organize Your Code With Functions

In this step, you’ll refactor your code. Refactoring means that you’ll change your code, but your application’s behavior and your user’s experience will stay as they are. This may not sound very exciting, but it’ll be tremendously useful down the line, as good refactorings make it more convenient to maintain and expand your code.

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:

Prepare Data

Many games and applications follow a common life cycle:

  1. Preprocess: Prepare initial data.
  2. Process: Run main loop.
  3. Postprocess: Clean up and close application.

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:

def run_quiz():
    # Preprocess
    questions = prepare_questions()

    # Process (main loop)
    num_correct = 0
    for question in questions:
        num_correct += ask_question(question)

    # Postprocess
    print(f"nYou got {num_correct} correct")

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:

# quiz.py

import random
from string import ascii_lowercase

NUM_QUESTIONS_PER_QUIZ = 5
QUESTIONS = {
    "What's one effect of calling random.seed(42)": [
        "The random numbers are reproducible.",
        "The random numbers are more random.",
        "The computer clock is reset.",
        "The first random number is always 42.",
    ],
    "When does __name__ == '__main__' equal True in a Python file": [
        "When the file is run as a script",
        "When the file is imported as a module",
        "When the file has a valid name",
        "When the file only has one function",
    ]
}

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:

# quiz.py

# ...

def prepare_questions(questions, num_questions):
    num_questions = min(num_questions, len(questions))
    return random.sample(list(questions.items()), k=num_questions)

Note that prepare_questions() deals with general questions and num_questions parameters. Afterward, you’ll pass in your specific QUESTIONS and NUM_QUESTIONS_PER_QUIZ as arguments. This means that prepare_questions() doesn’t depend on your global variables. With this decoupling, your function is more general, and you can later more readily replace the source of your questions.

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.

Think about what ask_question() needs to do:

  1. Pick out the correct answer from the list of alternatives
  2. Shuffle the alternatives
  3. Print the question to the screen
  4. Print all alternatives to the screen
  5. Get the answer from the user
  6. Check that the user’s answer is valid
  7. Check whether the user answered correctly or not
  8. Add 1 to the count of correct answers if the answer is correct

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:

# quiz.py

# ...

def get_answer(question, alternatives):
    print(f"{question}?")
    labeled_alternatives = dict(zip(ascii_lowercase, alternatives))
    for label, alternative in labeled_alternatives.items():
        print(f"  {label}) {alternative}")

    while (answer_label := input("nChoice? ")) not in labeled_alternatives:
        print(f"Please answer one of {', '.join(labeled_alternatives)}")

    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.

Using get_answer() simplifies your implementation of ask_question(), as you no longer need to handle the user interaction. You can do something like the following:

# quiz.py

# ...

def ask_question(question, alternatives):
    correct_answer = alternatives[0]
    ordered_alternatives = random.sample(alternatives, k=len(alternatives))

    answer = get_answer(question, ordered_alternatives)
    if answer == correct_answer:
        print("⭐ Correct! ⭐")
        return 1
    else:
        print(f"The answer is {correct_answer!r}, not {answer!r}")
        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.

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:

# quiz.py

# ...

def run_quiz():
    questions = prepare_questions(
        QUESTIONS, num_questions=NUM_QUESTIONS_PER_QUIZ
    )

    num_correct = 0
    for num, (question, alternatives) in enumerate(questions, start=1):
        print(f"nQuestion {num}:")
        num_correct += ask_question(question, alternatives)

    print(f"nYou got {num_correct} correct out of {num} questions")

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:

# 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:

The full source code of your quiz application is listed below:

# quiz.py

import random
from string import ascii_lowercase

NUM_QUESTIONS_PER_QUIZ = 5
QUESTIONS = {
    "When was the first known use of the word 'quiz'": [
        "1781", "1771", "1871", "1881",
    ],
    "Which built-in function can get information from the user": [
        "input", "get", "print", "write",
    ],
    "Which keyword do you use to loop over a given list of elements": [
        "for", "while", "each", "loop",
    ],
    "What's the purpose of the built-in zip() function": [
        "To iterate over two or more sequences at the same time",
        "To combine several strings into one",
        "To compress several files into one archive",
        "To get information from the user",
    ],
    "What's the name of Python's sorting algorithm": [
        "Timsort", "Quicksort", "Merge sort", "Bubble sort",
    ],
    "What does dict.get(key) return if key isn't found in dict": [
        "None", "key", "True", "False",
    ],
    "How do you iterate over both indices and elements in an iterable": [
        "enumerate(iterable)",
        "enumerate(iterable, start=1)",
        "range(iterable)",
        "range(iterable, start=1)",
    ],
    "What's the official name of the := operator": [
        "Assignment expression",
        "Named expression",
        "Walrus operator",
        "Colon equals operator",
    ],
    "What's one effect of calling random.seed(42)": [
        "The random numbers are reproducible.",
        "The random numbers are more random.",
        "The computer clock is reset.",
        "The first random number is always 42.",
    ],
    "When does __name__ == '__main__' equal True in a Python file": [
        "When the file is run as a script",
        "When the file is imported as a module",
        "When the file has a valid name",
        "When the file only has one function",
    ]
}

def run_quiz():
    questions = prepare_questions(
        QUESTIONS, num_questions=NUM_QUESTIONS_PER_QUIZ
    )

    num_correct = 0
    for num, (question, alternatives) in enumerate(questions, start=1):
        print(f"nQuestion {num}:")
        num_correct += ask_question(question, alternatives)

    print(f"nYou got {num_correct} correct out of {num} questions")

def prepare_questions(questions, num_questions):
    num_questions = min(num_questions, len(questions))
    return random.sample(list(questions.items()), k=num_questions)

def ask_question(question, alternatives):
    correct_answer = alternatives[0]
    ordered_alternatives = random.sample(alternatives, k=len(alternatives))

    answer = get_answer(question, ordered_alternatives)
    if answer == correct_answer:
        print("⭐ Correct! ⭐")
        return 1
    else:
        print(f"The answer is {correct_answer!r}, not {answer!r}")
        return 0

def get_answer(question, alternatives):
    print(f"{question}?")
    labeled_alternatives = dict(zip(ascii_lowercase, alternatives))
    for label, alternative in labeled_alternatives.items():
        print(f"  {label}) {alternative}")

    while (answer_label := input("nChoice? ")) not in labeled_alternatives:
        print(f"Please answer one of {', '.join(labeled_alternatives)}")

    return labeled_alternatives[answer_label]

if __name__ == "__main__":
    run_quiz()

Run your application with python quiz.py.

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.

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:

Move Questions to a TOML File

TOML is branded as “a config file format for humans” (Source). It’s designed to be readable by humans and uncomplicated to parse by computers. Information is represented in key-value pairs that can be mapped to a hash table data structure, like a Python dictionary.

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:

# questions.toml

"When does __name__ == '__main__' equal True in a Python file" = [
    "When the file is run as a script",
    "When the file is imported as a module",
    "When the file has a valid name",
    "When the file only has one function",
]

"Which version of Python is the first with TOML support built in" = [
    "3.11", "3.9", "3.10", "3.12"
]

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.

Before installing tomli, you should create and activate a virtual environment:

  • Windows
  • Linux + macOS
PS> python -m venv venv
PS> venvScriptsactivate
$ python -m venv venv
$ source venv/bin/activate

You can then install tomli with pip:

  • Windows
  • Linux + macOS
(venv) PS> python -m pip install tomli
(venv) $ python -m pip install tomli

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:

>>>

>>> import tomli
>>> with open("questions.toml", mode="rb") as toml_file:
...     questions = tomli.load(toml_file)
...

>>> questions
{"When does __name__ == '__main__' equal True in a Python file":
    ['When the file is run as a script',
     'When the file is imported as a module',
     'When the file has a valid name',
     'When the file only has one function'],
 'Which version of Python is the first with TOML support built-in':
    ['3.11', '3.9', '3.10', '3.12']}

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.

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:

# quiz.py

# ...

import pathlib
try:
    import tomllib
except ModuleNotFoundError:
    import tomli as tomllib

NUM_QUESTIONS_PER_QUIZ = 5
QUESTIONS_PATH = pathlib.Path(__file__).parent / "questions.toml"
QUESTIONS = tomllib.loads(QUESTIONS_PATH.read_text())

# ...

Instead of doing a plain import tomli like you did earlier, you wrap your import in a tryexcept 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.

Add Flexibility to Your Data Format

You’ve moved your question data out of your source code and into a dedicated data file format. One advantage of TOML over a regular Python dictionary is that you can add some more structure to your data while keeping it fairly readable and maintainable.

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:

[[questions]]
question = "Which version of Python is the first with TOML support built in"
answer = "3.11"
alternatives = ["3.9", "3.10", "3.12"]

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:

>>>

>>> toml = """
... [[questions]]
... question = "Which version of Python is the first with TOML support built in"
... answer = "3.11"
... alternatives = ["3.9", "3.10", "3.12"]
... """

>>> import tomli
>>> tomli.loads(toml)
{'questions': [
  {
    'question': 'Which version of Python is the first with TOML support built in',
    'answer': '3.11',
    'alternatives': ['3.9', '3.10', '3.12']
  }
]}

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:

# questions.toml

[[questions]]
question = "Which version of Python is the first with TOML support built in"
answer = "3.11"
alternatives = ["3.9", "3.10", "3.12"]

[[questions]]
question = "What's the name of the list-like data structure in TOML"
answer = "Array"
alternatives = ["List", "Sequence", "Set"]

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:

  1. Read questions from the inner questions list.
  2. Use the inner question dictionaries when asking questions.

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:

# quiz.py

# ...

NUM_QUESTIONS_PER_QUIZ = 5
QUESTIONS_PATH = pathlib.Path(__file__).parent / "questions.toml"

def run_quiz():
    questions = prepare_questions(
        QUESTIONS_PATH, num_questions=NUM_QUESTIONS_PER_QUIZ
    )

    num_correct = 0
    for num, question in enumerate(questions, start=1):
        print(f"nQuestion {num}:")
        num_correct += ask_question(question)

    print(f"nYou got {num_correct} correct out of {num} questions")

def prepare_questions(path, num_questions):
    questions = tomllib.loads(path.read_text())["questions"]
    num_questions = min(num_questions, len(questions))
    return random.sample(questions, k=num_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.

This latter point requires some changes in ask_question() as well:

# quiz.py

# ...

def ask_question(question):
    correct_answer = question["answer"]
    alternatives = [question["answer"]] + question["alternatives"]
    ordered_alternatives = random.sample(alternatives, k=len(alternatives))

    answer = get_answer(question["question"], ordered_alternatives)
    if answer == correct_answer:
        print("⭐ Correct! ⭐")
        return 1
    else:
        print(f"The answer is {correct_answer!r}, not {answer!r}")
        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:

The full questions.toml data file is reproduced below:

# questions.toml

[[questions]]
question = "When was the first known use of the word 'quiz'"
answer = "1781"
alternatives = ["1771", "1871", "1881"]

[[questions]]
question = "Which built-in function can get information from the user"
answer = "input"
alternatives = ["get", "print", "write"]

[[questions]]
question = "What's the purpose of the built-in zip() function"
answer = "To iterate over two or more sequences at the same time"
alternatives = [
    "To combine several strings into one",
    "To compress several files into one archive",
    "To get information from the user",
]

[[questions]]
question = "What does dict.get(key) return if key isn't found in dict"
answer = "None"
alternatives = ["key", "True", "False"]

[[questions]]
question = "How do you iterate over both indices and elements in an iterable"
answer = "enumerate(iterable)"
alternatives = [
    "enumerate(iterable, start=1)",
    "range(iterable)",
    "range(iterable, start=1)",
]

[[questions]]
question = "What's the official name of the := operator"
answer = "Assignment expression"
alternatives = ["Named expression", "Walrus operator", "Colon equals operator"]

[[questions]]
question = "What's one effect of calling random.seed(42)"
answer = "The random numbers are reproducible."
alternatives = [
    "The random numbers are more random.",
    "The computer clock is reset.",
    "The first random number is always 42.",
]

[[questions]]
question = "When does __name__ == '__main__' equal True in a Python file"
answer = "When the file is run as a script"
alternatives = [
    "When the file is imported as a module",
    "When the file has a valid name",
    "When the file only has one function",
]

[[questions]]
question = "Which version of Python is the first with TOML support built in"
answer = "3.11"
alternatives = ["3.9", "3.10", "3.12"]

[[questions]]
question = "What's the name of the list-like data structure in TOML"
answer = "Array"
alternatives = ["List", "Sequence", "Set"]

Save this file in the same folder as quiz.py.

The full source code of your quiz application is listed below:

# quiz.py

import pathlib
import random
from string import ascii_lowercase
try:
    import tomllib
except ModuleNotFoundError:
    import tomli as tomllib

NUM_QUESTIONS_PER_QUIZ = 5
QUESTIONS_PATH = pathlib.Path(__file__).parent / "questions.toml"

def run_quiz():
    questions = prepare_questions(
        QUESTIONS_PATH, num_questions=NUM_QUESTIONS_PER_QUIZ
    )

    num_correct = 0
    for num, question in enumerate(questions, start=1):
        print(f"nQuestion {num}:")
        num_correct += ask_question(question)

    print(f"nYou got {num_correct} correct out of {num} questions")

def prepare_questions(path, num_questions):
    questions = tomllib.loads(path.read_text())["questions"]
    num_questions = min(num_questions, len(questions))
    return random.sample(questions, k=num_questions)

def ask_question(question):
    correct_answer = question["answer"]
    alternatives = [question["answer"]] + question["alternatives"]
    ordered_alternatives = random.sample(alternatives, k=len(alternatives))

    answer = get_answer(question["question"], ordered_alternatives)
    if answer == correct_answer:
        print("⭐ Correct! ⭐")
        return 1
    else:
        print(f"The answer is {correct_answer!r}, not {answer!r}")
        return 0

def get_answer(question, alternatives):
    print(f"{question}?")
    labeled_alternatives = dict(zip(ascii_lowercase, alternatives))
    for label, alternative in labeled_alternatives.items():
        print(f"  {label}) {alternative}")

    while (answer_label := input("nChoice? ")) not in labeled_alternatives:
        print(f"Please answer one of {', '.join(labeled_alternatives)}")

    return labeled_alternatives[answer_label]

if __name__ == "__main__":
    run_quiz()

Run your application with python quiz.py.

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.

Step 5: Expand Your Quiz Functionality

In this fifth step, you’ll add more functionality to your quiz application. Finally, the refactoring you’ve done in the previous steps will pay off! You’ll add the following features:

  • Questions with multiple correct answers
  • Hints that can point toward the correct answer
  • Explanations that can act as teaching moments

At the end of this step, your application will work as follows:

These new features provide a more interesting experience to anyone challenging themselves through your quiz application. You can see how the source code of your application will look when you’re done with this step by clicking below and moving into the source_code_step_5 directory:

Allow Multiple Correct Answers

Some questions may have multiple correct answers, and it’ll be great if your quiz can handle those as well. In this section, you’ll add support for multiple correct answers.

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:

# questions.toml

[[questions]]
question = "What's the name of the list-like data structure in TOML"
answers = ["Array"]
alternatives = ["List", "Sequence", "Set"]

[[questions]]
question = "How can you run a Python script named quiz.py"
answers = ["python quiz.py", "python -m quiz"]
alternatives = ["python quiz", "python -m quiz.py"]

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:

>>>

>>> answer = "a,b, c"
>>> answer.replace(",", " ").split()
['a', 'b', 'c']

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:

# quiz.py

# ...

def get_answers(question, alternatives, num_choices=1):
    print(f"{question}?")
    labeled_alternatives = dict(zip(ascii_lowercase, alternatives))
    for label, alternative in labeled_alternatives.items():
        print(f"  {label}) {alternative}")

    while True:
        plural_s = "" if num_choices == 1 else f"s (choose {num_choices})"
        answer = input(f"nChoice{plural_s}? ")
        answers = set(answer.replace(",", " ").split())

        # Handle invalid answers
        if len(answers) != num_choices:
            plural_s = "" if num_choices == 1 else "s, separated by comma"
            print(f"Please answer {num_choices} alternative{plural_s}")
            continue

        if any(
            (invalid := answer) not in labeled_alternatives
            for answer in answers
        ):
            print(
                f"{invalid!r} is not a valid choice. "
                f"Please use {', '.join(labeled_alternatives)}"
            )
            continue

        return [labeled_alternatives[answer] for answer in answers]

Before looking too closely at the details in the code, take the function for a test run:

>>>

>>> from quiz import get_answers
>>> get_answers(
...     "Pick two numbers", ["one", "two", "three", "four"], num_choices=2
... )
Pick two numbers?
  a) one
  b) two
  c) three
  d) four

Choices (choose 2)? a
Please answer 2 alternatives, separated by comma

Choices (choose 2)? d, e
'e' is not a valid choice. Please use a, b, c, d

Choices (choose 2)? d, b
['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:

# quiz.py

# ...

def ask_question(question):
    correct_answers = question["answers"]
    alternatives = question["answers"] + question["alternatives"]
    ordered_alternatives = random.sample(alternatives, k=len(alternatives))

    answers = get_answers(
        question=question["question"],
        alternatives=ordered_alternatives,
        num_choices=len(correct_answers),
    )
    if set(answers) == set(correct_answers):
        print("⭐ Correct! ⭐")
        return 1
    else:
        is_or_are = " is" if len(correct_answers) == 1 else "s are"
        print("n- ".join([f"No, the answer{is_or_are}:"] + correct_answers))
        return 0

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:

$ python quiz.py

Question 1:
How can you run a Python script named quiz.py?
  a) python -m quiz
  b) python quiz
  c) python quiz.py
  d) python -m quiz.py

Choices (choose 2)? a
Please answer 2 alternatives, separated by comma

Choices (choose 2)? a, c
⭐ Correct! ⭐

Question 2:
What's the name of the list-like data structure in TOML?
  a) Array
  b) Set
  c) Sequence
  d) List

Choice? e
'e' is not a valid choice. Please use a, b, c, d

Choice? c
No, the answer is:
- Array

You got 1 correct out of 2 questions

Allowing multiple correct answers gives you more flexibility in which kinds of questions you can ask in your quizzes.

Add Hints to Help the User

Sometimes when you’re asked a question, you need a bit of help to jog your memory. 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.

You can include hints in your questions.toml data file, for example by adding hint as an optional key-value pair:

# questions.toml

[[questions]]
question = "How can you run a Python script named quiz.py"
answers = ["python quiz.py", "python -m quiz"]
alternatives = ["python quiz", "python -m quiz.py"]
hint = "One option uses the filename, and the other uses the module name."

[[questions]]
question = "What's a PEP"
answers = ["A Python Enhancement Proposal"]
alternatives = [
    "A Pretty Exciting Policy",
    "A Preciously Evolved Python",
    "A Potentially Epic Prize",
    ]
hint = "PEPs are used to evolve Python."

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:

# quiz.py

# ...

def ask_question(question):
    # ...
    answers = get_answers(
        question=question["question"],
        alternatives=ordered_alternatives,
        num_choices=len(correct_answers),
        hint=question.get("hint"),
    )
    # ...

You use question.get("hint") instead of question["hint"] because not all questions come with hints. If one of the question dictionaries doesn’t define "hint" as a key, then question.get("hint") returns None, which is then passed into get_answers().

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:

# quiz.py

# ...

def get_answers(question, alternatives, num_choices=1, hint=None):
    print(f"{question}?")
    labeled_alternatives = dict(zip(ascii_lowercase, alternatives))
    if hint:
        labeled_alternatives["?"] = "Hint"

    for label, alternative in labeled_alternatives.items():
        print(f"  {label}) {alternative}")

    while True:
        plural_s = "" if num_choices == 1 else f"s (choose {num_choices})"
        answer = input(f"nChoice{plural_s}? ")
        answers = set(answer.replace(",", " ").split())

        # Handle hints
        if hint and "?" in answers:
            print(f"nHINT: {hint}")
            continue

        # Handle invalid answers
        # ...

        return [labeled_alternatives[answer] for answer in answers]

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:

$ python quiz.py

Question 1:
What's a PEP?
  a) A Potentially Epic Prize
  b) A Preciously Evolved Python
  c) A Python Enhancement Proposal
  d) A Pretty Exciting Policy
  ?) Hint

Choice? ?

HINT: PEPs are used to evolve Python.

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.

Add Explanations to Reinforce Learning

You can implement explanations similarly to how you implemented hints in the previous section. First, you’ll add an optional explanation field in your data file. Then, in your application, you’ll show the explanation after the user has answered a question.

Start with adding explanation keys in questions.toml:

# questions.toml

[[questions]]
question = "What's a PEP"
answers = ["A Python Enhancement Proposal"]
alternatives = [
    "A Pretty Exciting Policy",
    "A Preciously Evolved Python",
    "A Potentially Epic Prize",
    ]
hint = "PEPs are used to evolve Python."
explanation = """
    Python Enhancement Proposals (PEPs) are design documents that provide
    information to the Python community. PEPs are used to propose new features
    for the Python language, to collect community input on an issue, and to
    document design decisions made about the language.
"""

[[questions]]
question = "How can you add a docstring to a function"
answers = [
    "By writing a string literal as the first statement in the function",
    "By assigning a string to the function's .__doc__ attribute",
]
alternatives = [
    "By using the built-in @docstring decorator",
    "By returning a string from the function",
]
hint = "They're parsed from your code and stored on the function object."
explanation = """
    Docstrings document functions and other Python objects. A docstring is a
    string literal that occurs as the first statement in a module, function,
    class, or method definition. Such a docstring becomes the .__doc__ special
    attribute of that object. See PEP 257 for more information.

    There is no built-in @docstring decorator. Many functions naturally return
    strings. Such a feature can therefore not be used for docstrings.
"""

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():

# quiz.py

# ...

def ask_question(question):
    correct_answers = question["answers"]
    alternatives = question["answers"] + question["alternatives"]
    ordered_alternatives = random.sample(alternatives, k=len(alternatives))

    answers = get_answers(
        question=question["question"],
        alternatives=ordered_alternatives,
        num_choices=len(correct_answers),
        hint=question.get("hint"),
    )
    if correct := (set(answers) == set(correct_answers)):
        print("⭐ Correct! ⭐")
    else:
        is_or_are = " is" if len(correct_answers) == 1 else "s are"
        print("n- ".join([f"No, the answer{is_or_are}:"] + correct_answers))

    if "explanation" in question:
        print(f"nEXPLANATION:n{question['explanation']}")

    return 1 if correct else 0

Because you print the explanation after giving the user feedback on whether their answer was correct or not, you can’t return inside the ifelse 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:

$ python quiz.py

Question 1:
How can you add a docstring to a function?
  a) By returning a string from the function
  b) By assigning a string to the function's .__doc__ attribute
  c) By writing a string literal as the first statement in the function
  d) By using the built-in @docstring decorator
  ?) Hint

Choices (choose 2)? a, b
No, the answers are:
- By writing a string literal as the first statement in the function
- By assigning a string to the function's .__doc__ attribute

EXPLANATION:
    Docstrings document functions and other Python objects. A docstring is a
    string literal that occurs as the first statement in a module, function,
    class, or method definition. Such a docstring becomes the .__doc__ special
    attribute of that object. See PEP 257 for more information.

    There is no built-in @docstring decorator. Many functions naturally return
    strings. Such a feature can therefore not be used for docstrings.

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:

The full questions.toml data file is reproduced below:

# questions.toml

[[questions]]
question = "When was the first known use of the word 'quiz'"
answers = ["1781"]
alternatives = ["1771", "1871", "1881"]

[[questions]]
question = "Which built-in function can get information from the user"
answers = ["input"]
alternatives = ["get", "print", "write"]

[[questions]]
question = "What's the purpose of the built-in zip() function"
answers = ["To iterate over two or more sequences at the same time"]
alternatives = [
    "To combine several strings into one",
    "To compress several files into one archive",
    "To get information from the user",
]

[[questions]]
question = "What does dict.get(key) return if key isn't found in dict"
answers = ["None"]
alternatives = ["key", "True", "False"]

[[questions]]
question = "How do you iterate over both indices and elements in an iterable"
answers = ["enumerate(iterable)"]
alternatives = [
    "enumerate(iterable, start=1)",
    "range(iterable)",
    "range(iterable, start=1)",
]

[[questions]]
question = "What's the official name of the := operator"
answers = ["Assignment expression"]
alternatives = ["Named expression", "Walrus operator", "Colon equals operator"]

[[questions]]
question = "What's one effect of calling random.seed(42)"
answers = ["The random numbers are reproducible."]
alternatives = [
    "The random numbers are more random.",
    "The computer clock is reset.",
    "The first random number is always 42.",
]

[[questions]]
question = "When does __name__ == '__main__' equal True in a Python file"
answers = ["When the file is run as a script"]
alternatives = [
    "When the file is imported as a module",
    "When the file has a valid name",
    "When the file only has one function",
]

[[questions]]
question = "Which version of Python is the first with TOML support built in"
answers = ["3.11"]
alternatives = ["3.9", "3.10", "3.12"]

[[questions]]
question = "What's the name of the list-like data structure in TOML"
answers = ["Array"]
alternatives = ["List", "Sequence", "Set"]

[[questions]]
question = "How can you run a Python script named quiz.py"
answers = ["python quiz.py", "python -m quiz"]
alternatives = ["python quiz", "python -m quiz.py"]
hint = "One option uses the filename, and the other uses the module name."

[[questions]]
question = "What's a PEP"
answers = ["A Python Enhancement Proposal"]
alternatives = [
    "A Pretty Exciting Policy",
    "A Preciously Evolved Python",
    "A Potentially Epic Prize",
]
hint = "PEPs are used to evolve Python."
explanation = """
    Python Enhancement Proposals (PEPs) are design documents that provide
    information to the Python community. PEPs are used to propose new features
    for the Python language, to collect community input on an issue, and to
    document design decisions made about the language.
"""

[[questions]]
question = "How can you add a docstring to a function"
answers = [
    "By writing a string literal as the first statement in the function",
    "By assigning a string to the function's .__doc__ attribute",
]
alternatives = [
    "By using the built-in @docstring decorator",
    "By returning a string from the function",
]
hint = "They are parsed from your code and stored on the function object."
explanation = """
    Docstrings document functions and other Python objects. A docstring is a
    string literal that occurs as the first statement in a module, function,
    class, or method definition. Such a docstring becomes the .__doc__ special
    attribute of that object. See PEP 257 for more information.

    There is no built-in @docstring decorator. Many functions naturally return
    strings. Such a feature can therefore not be used for docstrings.
"""

Save this file in the same folder as quiz.py.

The full source code of your quiz application is listed below:

# quiz.py

import pathlib
import random
from string import ascii_lowercase
try:
    import tomllib
except ModuleNotFoundError:
    import tomli as tomllib

NUM_QUESTIONS_PER_QUIZ = 5
QUESTIONS_PATH = pathlib.Path(__file__).parent / "questions.toml"

def run_quiz():
    questions = prepare_questions(
        QUESTIONS_PATH, num_questions=NUM_QUESTIONS_PER_QUIZ
    )

    num_correct = 0
    for num, question in enumerate(questions, start=1):
        print(f"nQuestion {num}:")
        num_correct += ask_question(question)

    print(f"nYou got {num_correct} correct out of {num} questions")

def prepare_questions(path, num_questions):
    questions = tomllib.loads(path.read_text())["questions"]
    num_questions = min(num_questions, len(questions))
    return random.sample(questions, k=num_questions)

def ask_question(question):
    correct_answers = question["answers"]
    alternatives = question["answers"] + question["alternatives"]
    ordered_alternatives = random.sample(alternatives, k=len(alternatives))

    answers = get_answers(
        question=question["question"],
        alternatives=ordered_alternatives,
        num_choices=len(correct_answers),
        hint=question.get("hint"),
    )
    if correct := (set(answers) == set(correct_answers)):
        print("⭐ Correct! ⭐")
    else:
        is_or_are = " is" if len(correct_answers) == 1 else "s are"
        print("n- ".join([f"No, the answer{is_or_are}:"] + correct_answers))

    if "explanation" in question:
        print(f"nEXPLANATION:n{question['explanation']}")

    return 1 if correct else 0

def get_answers(question, alternatives, num_choices=1, hint=None):
    print(f"{question}?")
    labeled_alternatives = dict(zip(ascii_lowercase, alternatives))
    if hint:
        labeled_alternatives["?"] = "Hint"

    for label, alternative in labeled_alternatives.items():
        print(f"  {label}) {alternative}")

    while True:
        plural_s = "" if num_choices == 1 else f"s (choose {num_choices})"
        answer = input(f"nChoice{plural_s}? ")
        answers = set(answer.replace(",", " ").split())

        # Handle hints
        if hint and "?" in answers:
            print(f"nHINT: {hint}")
            continue

        # Handle invalid answers
        if len(answers) != num_choices:
            plural_s = "" if num_choices == 1 else "s, separated by comma"
            print(f"Please answer {num_choices} alternative{plural_s}")
            continue

        if any(
            (invalid := answer) not in labeled_alternatives
            for answer in answers
        ):
            print(
                f"{invalid!r} is not a valid choice. "
                f"Please use {', '.join(labeled_alternatives)}"
            )
            continue

        return [labeled_alternatives[answer] for answer in answers]

if __name__ == "__main__":
    run_quiz()

Run your application with python quiz.py.

In the final step, you’ll add one more feature: support for several quiz topics within your application.

Step 6: Support Several Quiz Topics

In this section, you’ll make one final improvement that will make your Python quiz application more fun, varied, and interesting. You’ll add the option of grouping questions into different topics and letting your users pick which topic they’ll be quizzed about.

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:

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:

>>>

>>> toml = """
... [python]
... label = "Python"
...
... [python.version]
... number = "3.10"
... release.date = 2021-10-04
... release.manager = "@pyblogsal"
... """

>>> import tomli
>>> tomli.loads(toml)
{'python': {'label': 'Python', 'version': {
    'release': {'date': datetime.date(2021, 10, 4), 'manager': '@pyblogsal'},
    'number': '3.10'}}}

Here, the section header [python.version] is represented as version nested within python. Similarly, keys with periods are also interpreted as nested dictionaries, as evidenced by release in this example.

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:

# questions.toml

[python]
label = "Python"

[[python.questions]]
question = "How can you add a docstring to a function"
answers = [
    "By writing a string literal as the first statement in the function",
    "By assigning a string to the function's .__doc__ attribute",
]
alternatives = [
    "By using the built-in @docstring decorator",
    "By returning a string from the function",
]
hint = "They're parsed from your code and stored on the function object."
explanation = """
    Docstrings document functions and other Python objects. A docstring is a
    string literal that occurs as the first statement in a module, function,
    class, or method definition. Such a docstring becomes the .__doc__ special
    attribute of that object. See PEP 257 for more information.

    There's no built-in @docstring decorator. Many functions naturally return
    strings. Such a feature can therefore not be used for docstrings.
"""

[[python.questions]]
question = "When was the first public version of Python released?"
answers = ["February 1991"]
alternatives = ["January 1994", "October 2000", "December 2008"]
hint = "The first public version was labeled version 0.9.0."
explanation = """
    Guido van Rossum started work on Python in December 1989. He posted
    Python v0.9.0 to the alt.sources newsgroup in February 1991. Python
    reached version 1.0.0 in January 1994. The next major versions,
    Python 2.0 and Python 3.0, were released in October 2000 and December
    2008, respectively.
"""

[capitals]
label = "Capitals"

[[capitals.questions]]
question = "What's the capital of Norway"
answers = ["Oslo"]
hint = "Lars Onsager, Jens Stoltenberg, Trygve Lie, and Børge Ousland."
alternatives = ["Stockholm", "Copenhagen", "Helsinki", "Reykjavik"]
explanation = """
    Oslo was founded as a city in the 11th century and established as a
    trading place. It became the capital of Norway in 1299. The city was
    destroyed by a fire in 1624 and rebuilt as Christiania, named in honor
    of the reigning king. The city was renamed back to Oslo in 1925.
"""

[[capitals.questions]]
question = "What's the state capital of Texas, USA"
answers = ["Austin"]
alternatives = ["Harrisburg", "Houston", "Galveston", "Columbia"]
hint = "SciPy is held there each year."
explanation = """
    Austin is named in honor of Stephen F. Austin. It was purpose-built to
    be the capital of Texas and was incorporated in December 1839. Houston,
    Harrisburg, Columbia, and Galveston are all earlier capitals of Texas.
"""

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:

# quiz.py

# ...

def prepare_questions(path, num_questions):
    topic_info = tomllib.loads(path.read_text())
    topics = {
        topic["label"]: topic["questions"] for topic in topic_info.values()
    }
    topic_label = get_answers(
        question="Which topic do you want to be quizzed about",
        alternatives=sorted(topics),
    )[0]

    questions = topics[topic_label]
    num_questions = min(num_questions, len(questions))
    return random.sample(questions, k=num_questions)

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:

$ python quiz.py
Which topic do you want to be quizzed about?
  a) Capitals
  b) Python

Choice? a

Question 1:
What's the capital of Norway?
  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:
    Oslo was founded as a city in the 11th century and established as a
    trading place. It became the capital of Norway in 1299. The city was
    destroyed by a fire in 1624 and rebuilt as Christiania, named in honor
    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:

The full questions.toml data file is reproduced below:

# questions.toml

[python]
label = "Python"

[[python.questions]]
question = "When was the first known use of the word 'quiz'"
answers = ["1781"]
alternatives = ["1771", "1871", "1881"]

[[python.questions]]
question = "Which built-in function can get information from the user"
answers = ["input"]
alternatives = ["get", "print", "write"]

[[python.questions]]
question = "What's the purpose of the built-in zip() function"
answers = ["To iterate over two or more sequences at the same time"]
alternatives = [
    "To combine several strings into one",
    "To compress several files into one archive",
    "To get information from the user",
]

[[python.questions]]
question = "What does dict.get(key) return if key isn't found in dict"
answers = ["None"]
alternatives = ["key", "True", "False"]

[[python.questions]]
question = "How do you iterate over both indices and elements in an iterable"
answers = ["enumerate(iterable)"]
alternatives = [
    "enumerate(iterable, start=1)",
    "range(iterable)",
    "range(iterable, start=1)",
]

[[python.questions]]
question = "What's the official name of the := operator"
answers = ["Assignment expression"]
alternatives = [
    "Named expression",
    "Walrus operator",
    "Colon equals operator",
]

[[python.questions]]
question = "What's one effect of calling random.seed(42)"
answers = ["The random numbers are reproducible."]
alternatives = [
    "The random numbers are more random.",
    "The computer clock is reset.",
    "The first random number is always 42.",
]

[[python.questions]]
question = "Which version of Python is the first with TOML support built in"
answers = ["3.11"]
alternatives = ["3.9", "3.10", "3.12"]

[[python.questions]]
question = "How can you run a Python script named quiz.py"
answers = ["python quiz.py", "python -m quiz"]
alternatives = ["python quiz", "python -m quiz.py"]
hint = "One option uses the filename, and the other uses the module name."

[[python.questions]]
question = "What's the name of the list-like data structure in TOML"
answers = ["Array"]
alternatives = ["List", "Sequence", "Set"]

[[python.questions]]
question = "What's a PEP"
answers = ["A Python Enhancement Proposal"]
alternatives = [
    "A Pretty Exciting Policy",
    "A Preciously Evolved Python",
    "A Potentially Epic Prize",
]
hint = "PEPs are used to evolve Python."
explanation = """
Python Enhancement Proposals (PEPs) are design documents that provide
information to the Python community. PEPs are used to propose new features
for the Python language, to collect community input on an issue, and to
document design decisions made about the language.
"""

[[python.questions]]
question = "How can you add a docstring to a function"
answers = [
    "By writing a string literal as the first statement in the function",
    "By assigning a string to the function's .__doc__ attribute",
]
alternatives = [
    "By using the built-in @docstring decorator",
    "By returning a string from the function",
]
hint = "They are parsed from your code and stored on the function object."
explanation = """
Docstrings document functions and other Python objects. A docstring is a
string literal that occurs as the first statement in a module, function,
class, or method definition. Such a docstring becomes the .__doc__ special
attribute of that object. See PEP 257 for more information.

There's no built-in @docstring decorator. Many functions naturally return
strings. Such a feature can therefore not be used for docstrings.
"""

[[python.questions]]
question = "When was the first public version of Python released"
answers = ["February 1991"]
alternatives = ["January 1994", "October 2000", "December 2008"]
hint = "The first public version was labeled version 0.9.0."
explanation = """
Guido van Rossum started work on Python in December 1989. He posted
Python v0.9.0 to the alt.sources newsgroup in February 1991. Python
reached version 1.0.0 in January 1994. The next major versions,
Python 2.0 and Python 3.0, were released in October 2000 and December
2008, respectively.
"""

[capitals]
label = "Capitals"

[[capitals.questions]]
question = "What's the capital of Norway"
answers = ["Oslo"]
hint = "Lars Onsager, Jens Stoltenberg, Trygve Lie, and Børge Ousland."
alternatives = ["Stockholm", "Copenhagen", "Helsinki", "Reykjavik"]
explanation = """
Oslo was founded as a city in the 11th century and established as a
trading place. It became the capital of Norway in 1299. The city was
destroyed by a fire in 1624 and rebuilt as Christiania, named in honor
of the reigning king. The city was renamed back to Oslo in 1925.
"""

[[capitals.questions]]
question = "What's the state capital of Texas, USA"
answers = ["Austin"]
alternatives = ["Harrisburg", "Houston", "Galveston", "Columbia"]
hint = "SciPy is held there each year."
explanation = """
Austin is named in honor of Stephen F. Austin. It was purpose-built to
be the capital of Texas and was incorporated in December 1839. Houston,
Harrisburg, Columbia, and Galveston are all earlier capitals of Texas.
"""

Save this file in the same folder as quiz.py.

The full source code of your quiz application is listed here:

# quiz.py

import pathlib
import random
from string import ascii_lowercase
try:
    import tomllib
except ModuleNotFoundError:
    import tomli as tomllib

NUM_QUESTIONS_PER_QUIZ = 5
QUESTIONS_PATH = pathlib.Path(__file__).parent / "questions.toml"

def run_quiz():
    questions = prepare_questions(
        QUESTIONS_PATH, num_questions=NUM_QUESTIONS_PER_QUIZ
    )

    num_correct = 0
    for num, question in enumerate(questions, start=1):
        print(f"nQuestion {num}:")
        num_correct += ask_question(question)

    print(f"nYou got {num_correct} correct out of {num} questions")

def prepare_questions(path, num_questions):
    topic_info = tomllib.loads(path.read_text())
    topics = {
        topic["label"]: topic["questions"] for topic in topic_info.values()
    }
    topic_label = get_answers(
        question="Which topic do you want to be quizzed about",
        alternatives=sorted(topics),
    )[0]

    questions = topics[topic_label]
    num_questions = min(num_questions, len(questions))
    return random.sample(questions, k=num_questions)

def ask_question(question):
    correct_answers = question["answers"]
    alternatives = question["answers"] + question["alternatives"]
    ordered_alternatives = random.sample(alternatives, k=len(alternatives))

    answers = get_answers(
        question=question["question"],
        alternatives=ordered_alternatives,
        num_choices=len(correct_answers),
        hint=question.get("hint"),
    )
    if correct := (set(answers) == set(correct_answers)):
        print("⭐ Correct! ⭐")
    else:
        is_or_are = " is" if len(correct_answers) == 1 else "s are"
        print("n- ".join([f"No, the answer{is_or_are}:"] + correct_answers))

    if "explanation" in question:
        print(f"nEXPLANATION:n{question['explanation']}")

    return 1 if correct else 0

def get_answers(question, alternatives, num_choices=1, hint=None):
    print(f"{question}?")
    labeled_alternatives = dict(zip(ascii_lowercase, alternatives))
    if hint:
        labeled_alternatives["?"] = "Hint"

    for label, alternative in labeled_alternatives.items():
        print(f"  {label}) {alternative}")

    while True:
        plural_s = "" if num_choices == 1 else f"s (choose {num_choices})"
        answer = input(f"nChoice{plural_s}? ")
        answers = set(answer.replace(",", " ").split())

        # Handle hints
        if hint and "?" in answers:
            print(f"nHINT: {hint}")
            continue

        # Handle invalid answers
        if len(answers) != num_choices:
            plural_s = "" if num_choices == 1 else "s, separated by comma"
            print(f"Please answer {num_choices} alternative{plural_s}")
            continue

        if any(
            (invalid := answer) not in labeled_alternatives
            for answer in answers
        ):
            print(
                f"{invalid!r} is not a valid choice. "
                f"Please use {', '.join(labeled_alternatives)}"
            )
            continue

        return [labeled_alternatives[answer] for answer in answers]

if __name__ == "__main__":
    run_quiz()

Run your application with python quiz.py.

You can also access the source code and the questions file by clicking below:

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.

In this tutorial, you’ve learned how to:

  • Interact with the user in the terminal
  • Improve the usability of your application
  • Refactor your application to continuously improve it
  • Store data in dedicated data files

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.

Here are some ideas for additional features:

  • 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 or create a flashcards app to help you prepare for your quizzes. Feel free to share your improvements in the comments below.

Время на прочтение
6 мин

Количество просмотров 22K

Меня зовут Егор, я Full-stack разработчик в Leader-ID. В этой статье я хочу поделиться простым рецептом по созданию красивого и удобного веб-опросника наподобие тех, что делает Meduza. Он умеет показывать статистику после ответа на отдельные вопросы, подсчитывать общий балл, выдавать комментарии, выгружать данные для анализа и шарить результаты в соцсети. Для реализации этой задачи я выбрал Django, DRF, Python и базу данных PostgreSQL.

Все детали — под катом.

Спустя час разглядывания кирпичной кладки

(залипательное занятие, однако)

появился первый результат в виде готовых моделей, которые спустя десять минут были описаны в Джанге.

Если ты начинающий, то советую пройти Djnago tutorial, там как раз описывается пошаговое создание опроса. И вдогонку DRF tutorial, чтобы окончательно погрузиться в тему.

Итак, в проекте я использовал:

  • Django 3.0.3. Для бэкенда;
  • django-rest-framework. Для создания rest-api;
  • Python;
  • PostgreSQL в качестве БД;
  • Front-end — Nuxt.js, Axios, Element-UI.

Теперь по шагам

pip install Django — устанавливаем библиотеку.

django-admin startproject core — создаем проект на джанге.

cd core — переходим в директорию с проектом.

python manage.py startapp polls — добавляем приложение опроса.

Далее описываем модели в models.py в polls и создаем сериалайзер для DRF.

class Question(models.Model):
    title = models.CharField(max_length=4096)
    visible = models.BooleanField(default=False)
    max_points = models.FloatField()

    def __str__(self):
           return self.title

class Choice(models.Model):
    question = models.ForeignKey(Question, on_delete=models.DO_NOTHING)
    title = models.CharField(max_length=4096)
    points = models.FloatField()
    lock_other = models.BooleanField(default=False)

    def __str__(self):
        return self.title

class Answer(models.Model):
    user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.DO_NOTHING)
    question = models.ForeignKey(Question, on_delete=models.DO_NOTHING)
    choice = models.ForeignKey(Choice, on_delete=models.DO_NOTHING)
    created = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return self.choice.title

Код текстом тут

from rest_framework import serializers
from .models import Answer, Question, Choice


class ChoiceSerializer(serializers.ModelSerializer):
    percent = serializers.SerializerMethodField()

    class Meta:
        model = Choice
        fields = ['pk', 'title', 'points', 'percent', 'lock_other', ]

    def get_percent(self, obj):
        total = Answer.objects.filter(question=obj.question).count()
        current = Answer.objects.filter(question=obj.question, choice=obj).count()
        if total != 0:
            return float(current * 100 / total)
        else:
            return float(0)


class QuestionSerializer(serializers.ModelSerializer):
    choices = ChoiceSerializer(many=True, source='choice_set', )

    class Meta:
        model = Question
        fields = ['pk', 'title', 'choices', 'max_points', ]


class AnswerSerializer(serializers.Serializer):
    answers = serializers.JSONField()

    def validate_answers(self, answers):
        if not answers:
            raise serializers.Validationerror("Answers must be not null.")
        return answers

    def save(self):
        answers = self.data['answers']
        user = self.context.user
        for question_id, in answers:  # тут наверное лишняя запятая , ошибка в оригинальном коде
            question = Question.objects.get(pk=question_id)
            choices = answers[question_id]
            for choice_id in choices:
                choice = Choice.objects.get(pk=choice_id)
                Answer(user=user, question=question, choice=choice).save()
                user.is_answer = True
                user.save()

Затем пишем две вьюшки DRF в views.py, которые отдают все вопросы с вариантами и принимают все ответы от пользователя.

Код текстом тут

from .serializers import QuestionSerializer, AnswerSerializer
from rest_framework.permissions import IsAuthenticated
from rest_framework.generics import GenericAPIView
from rest_framework.response import Response
from .models import Question


class GetQuestion(GenericAPIView):
    permission_classes = (IsAuthenticated,)
    serializer_class = QuestionSerializer

    def get(self, request, format=None):
        questions = Question.objects.filter(visible=True, )
        last_point = QuestionSerializer(questions, many=True)
        return Response(last_point.data)


class QuestionAnswer(GenericAPIView):
    permission_classes = (IsAuthenticated,)
    serializer_class = AnswerSerializer

    def post(self, request, format=None):
        answer = AnswerSerializer(data=request.data, context=request)
        if answer.is_valid(raise_exception=True):
            answer.save()
            return Response({'result': 'OK'})

Теперь описываем ссылки в urls.py:

urlpatterns = [
    path('', GetQuestion.as_view()),
    path('answer/', QuestionAnswer.as_view()),
]

Добавляем модели в admin.py:

Код текстом тут

from django.contrib import admin
from .models import Question, Answer, Choice


class QuestionAdmin(admin.ModelAdmin):
    list_display = (
        'title',
        'visible',
        'max_points',
    )


class ChoiceAdmin(admin.ModelAdmin):
    list_display = (
        'title',
        'question',
        'points',
        'lock_other',
    )
    list_filter = ('question',)


class AnswerAdmin(admin.ModelAdmin):
    list_display = (
        'user',
        'question',
        'choice',
    )
    list_filter = ('user',)


admin.site.register(Question, QuestionAdmin)
admin.site.register(Choice, ChoiceAdmin)
admin.site.register(Answer, AnswerAdmin)

Следующим шагом добавляем в settings.py (в директории core) в INSTALLED_APPS наше приложение polls. И выполняем команды запуска:

  • python manage.py makemigrations — создаем миграцию для созданных моделей
  • python manage.py migrate — выполняем миграцию в БД
  • python manage.py createsuperuser — создаем суперюзера (админа)
  • python manage.py runserver — запускаем сервер

Чтобы рассчитать общий балл по вопросам, добавляем к методу простенькую функцию обсчета, по которой рассчитывается начисление баллов. Код функции я не выложу, потому что с ее помощью можно взломать наш опросник. Теперь у каждого ответа есть свой «вес».

Заходим в админку через браузер по ссылке, которая указана в консоли (http://127.0.0.1:8000/admin по умолчанию), и создаем вопросы и ответы к ним, проставляем баллы.

Мне было важно отдавать нашим партнерам списки людей, прошедших опрос, и их ответы. Но для этого недостаточно просто связать ответы с вопросами. Поэтому я добавил еще одну таблицу — «Варианты». Так образовалась связь между ответами юзера на вопросы с несколькими вариантами ответов. Это позволяет нам выгружать данные в том виде, в котором партнеры могут их легко интерпретировать.

В итоге структура БД получилась вот такой:

Теперь подключаем фронт.

В нем забираем список вопросов и ответов, проходимся по каждому элементу до последнего. В зависимости от типа вопроса меняем компонент со своей логикой и стилем. Соответственно, когда вопросов в списке не осталось, отправляем результат на бэк и получаем ответ с количеством баллов. После получения количества баллов открываем страницу результата, в случае наличия баллов вопросы более не показываем.

На данном этапе у нас уже готов опросник, который умеет все, что должен: задавать вопросы, получать и собирать варианты ответов, выдавать результат в виде баллов и комментариев.

Добавляем плюшки

Во-первых, мне было необходимо периодически выгружать данные. Для этого я просто добавил management command.

Во-вторых, хорошо бы еще реализовать шаринг результатов опроса в социальные сети. ОК. Пилим функционал, который позволит поделиться картинкой с баллами ВКонтакте и Facebook.

Генерим сто вариантов картинок, отражающих баллы, для ВК и Facebook отдельно (разные разрешения). Теперь подключаем передачу ссылки на картинку в социальном компоненте фронтенд части. С ВКонтаке все оказалось просто: передаем параметр image с прямым URL-адресом нужной. А вот с Facebook пришлось повозиться. Оказалось, что они не принимают медиа по API, и если я передавал image или picture с URL картинки, то в посте показывалось большое пустое поле. Как потом оказалось, берет он картинку из метаинфы (og:image) самого сайта, которым поделились (передаем в ссылке параметр u). А ее, ко всему прочему, нужно было динамично менять. Мне не хотелось делать лишних редиректов и механик на бэке, и я решил переделать SPA (single page app) на SSR (server-side render) на фронте, чтобы в зависимости от запроса менялся url картинки с баллом в head-meta до запуска JavaScript в браузере. Благо, взятый за основу фреймворк Nuxt.js позволяет сделать это простым переключением режима. Теперь осталось набросать client-only теги и добавить логику смены head от наличия query балла.

Дополнительно на сервере понадобилось запустить daemon сервис, чтобы отдавать сформированные страницы, а статику оставить так же nginxу. Все, профит!

Оживляем опросник

Для того, чтобы поддерживать уровень интереса участников в процессе заполнения опроса, я добавил динамический показ статистики к каждому отдельному вопросу. Ответив на вопрос, пользователь видит, как ответили другие. Иногда человеку бывает непонятно, зачем ему задают эти вопросы. Поэтому я дополнил каждый вопрос забавными пояснениями. Ну и самый главный трюк по оживлению моего опросника провернули дизайнеры нашей компании.

Итоги

Такие медийные опросы достаточно просты в реализации и, главное, они очень нравятся пользователям. Их можно использовать и в хвост и в гриву: для социологических исследований, информирования/проверки знаний или создания интерактивных элементов на сайтах и сервисах. Я постарался подробно описать процесс их создания, но если остались вопросы, welcome в комментарии. Примеры реализации опросов на этом движке можно посмотреть по этим двум ссылкам: healthcare.leader-id.ru и covid.leader-id.ru.

How to Build a GUI Quiz App Using Tkinter and Open Trivia DB

In this article, we’ll learn to build a Graphical User Interface (GUI) Quiz Application using the Tkinter Python built-in module.

The task is to ask multiple-choice questions, collect user answers, and finally display the results.

Before coding the GUI, we’ll first see how to fetch multiple-choice questions, their correct answers, and the choices from the Open Trivia DB API.

The Open Trivia Database provides a completely free JSON API that you can use in your programming projects. Use of this API does not require an API Key. To make the task more interesting, we’ll also randomize the order of choices.

Watch this video to see what we are building:

We’ll use the following modules and concepts in this project:

  • tkinter is a standard GUI library for Python using which we can build desktop apps. This is the base of our project and we’ll use it to create the User Interface of the application.
  • random module implements pseudo-random number generators for various distributions. This module will help us shuffle the options for the questions.
  • requests library allows us to send HTTP/1.1 requests extremely easily. We’ll need the library to fetch questions from the Open Trivia DB.
  • Python Classes are a blueprint for creating objects. Objects are real-world entities. During the entire project development, we’ll be separating our different functionalities into different classes and methods.

Workflow of the Project

The basic workflow of the application will go like this:

  1. We’ll fetch questions from the Open Trivia DB API.
  2. For each fetched question, we’ll create a different object using a Question class. All these Question objects will be appended to a question_bank list.
  3. This question_bank will be passed to the brain of the application, QuizBrain and a quiz object will be created. This class will be responsible for checking if there are more questions, for getting the next question, calculating the score, and so on.
  4. Finally, this quiz object will be passed to the QuizInterface class, and the user will be able to interact with it.

Alright, let’s get started!

How to Fetch Questions from the Open Trivia DB API

As we discussed above, we’ll be using the Open Trivia DB API to get the questions. Head over to their API, select the number of questions you want, along with the categories and difficulty.

The question type should be Multiple Choice and the encoding should be Default Encoding. Click on Generate API URL and you’ll get an API URL.

Here’s a sample API URL: https://opentdb.com/api.php?amount=10&type=multiple

For fetching the questions, we’ll be using the requests module. You can install it like this:

$ pip install requests

Let’s create a Python file quiz_data.py to fetch the quiz questions and answers using the API URL generated above.

import requests

parameters = {
    "amount": 10,
    "type": "multiple"
}

response = requests.get(url="https://opentdb.com/api.php", params=parameters)
question_data = response.json()["results"]

In the above script, instead of directly adding the amount and type parameters in the URL, we have created a parameters dictionary and added the respective values.

After that, we’re making a GET request using the requests library on the Open Trivia DB API URL. A sample JSON response looks like this:

{
  "response_code": 0,
  "results": [
    {
      "category": "Entertainment: Video Games",
      "type": "multiple",
      "difficulty": "hard",
      "question": "What was the name of the hero in the 80s animated video game 'Dragon's Lair'?",
      "correct_answer": "Dirk the Daring",
      "incorrect_answers": ["Arthur", "Sir Toby Belch", "Guy of Gisbourne"]
    },
    {
      "category": "Entertainment: Video Games",
      "type": "multiple",
      "difficulty": "medium",
      "question": "Which of these game franchises were made by Namco?",
      "correct_answer": "Tekken",
      "incorrect_answers": ["Street Fighter", "Mortal Kombat", "Dragon Quest"]
    }
  ]
}

The JSON data contains a dictionary with two keys: response_code and results. The response_code tells developers what the API is doing. The results is a list we are interested in. So, we have stored the value of results in a variable called question_data.

How to Create the Question Model

Question model is nothing but a Python class with three attributes – question_text, correct_answer and choices.

question_text is the question, correct_answer is the correct answer for that question, and choices is a list of options for that question.

Let’s create a question_model.py file and create the class in it:

class Question:
    def __init__(self, question: str, correct_answer: str, choices: list):
        self.question_text = question
        self.correct_answer = correct_answer
        self.choices = choices

How to Create the Quiz Brain

The QuizBrain, as the name suggests, is the brain of the application. Let’s create a quiz_brain.py file and add the following code there:

class QuizBrain:

    def __init__(self, questions):
        self.question_no = 0
        self.score = 0
        self.questions = questions
        self.current_question = None

    def has_more_questions(self):
        """To check if the quiz has more questions"""
        
        return self.question_no < len(self.questions)

    def next_question(self):
        """Get the next question by incrementing the question number"""
        
        self.current_question = self.questions[self.question_no]
        self.question_no += 1
        q_text = self.current_question.question_text
        return f"Q.{self.question_no}: {q_text}"

    def check_answer(self, user_answer):
        """Check the user's answer against the correct answer and maintain the score"""
        
        correct_answer = self.current_question.correct_answer
        if user_answer.lower() == correct_answer.lower():
            self.score += 1
            return True
        else:
            return False

    def get_score(self):
        """Get the number of correct answers, wrong answers, and score percentage."""
        
        wrong = self.question_no - self.score
        score_percent = int(self.score / self.question_no * 100)
        return (self.score, wrong, score_percent)

The QuizBrain class takes questions, a list of questions. Additionally, the question_no and score attributes are initialized with 0 and the current_question is set to None initially.

The first method has_more_questions() checks whether the quiz has more questions or not.

The next method next_question() gets the question from the questions list at index question_no and then increments the question_no attribute and returns a formatted question.

The check_answer() method takes user_answer as an argument and checks whether the user’s answer is correct or not. It also maintains the score and returns boolean values.

The last method get_score() returns the number of correct answers, wrong answers, and score percentage.

How to Build the Quiz UI

Let’s move on to the next part where we’ll create the user interface of the application. Create a quiz_ui.py file for this section, and add the following code.

class QuizInterface:

    def __init__(self, quiz_brain: QuizBrain) -> None:
        self.quiz = quiz_brain
        self.window = Tk()
        self.window.title("iQuiz App")
        self.window.geometry("850x530")

        # Display Title
        self.display_title()

        # Creating a canvas for question text, and dsiplay question
        self.canvas = Canvas(width=800, height=250)
        self.question_text = self.canvas.create_text(400, 125,
                                                     text="Question here",
                                                     width=680,
                                                     fill=THEME_COLOR,
                                                     font=(
                                                         'Ariel', 15, 'italic')
                                                     )
        self.canvas.grid(row=2, column=0, columnspan=2, pady=50)
        self.display_question()

        # Declare a StringVar to store user's answer
        self.user_answer = StringVar()

        # Display four options(radio buttons)
        self.opts = self.radio_buttons()
        self.display_options()

        # To show whether the answer is correct or wrong
        self.feedback = Label(self.window, pady=10, font=("ariel", 15, "bold"))
        self.feedback.place(x=300, y=380)

        # Next and Quit Button
        self.buttons()

        # Mainloop
        self.window.mainloop()

In the above code, we’ve created a QuizInterface class with a constructor. In Python, the __init__() method is called constructor and is called automatically whenever an object of that class is created.

As discussed in the workflow, the QuizInterface class takes an argument of type QuizBrain. So, within the constructor, we have passed that as quiz_brain.

The first thing we do in Tkinter is create a window using the Tk class. You can set the title and the geometry using the title() and geometry() methods respectively.

Next we called few methods which we’ll be creating next. Apart from that, we’ve created a canvas using the Canvas class where our questions will be placed. Canvas is a rectangular area where we can place text, graphics, widgets, and so on.

Inside the canvas, we added a sample text for now using the create_text() method. We then declared a StringVar variable called user_answer to store the user’s answer in String type.

Next we created a feedback label to show whether the answer is right or wrong using the Label widget. This widget implements a display box where we can place text or images. You can update the text displayed by this widget at any time you want.

At the very end, we enter the main event loop to take action against each event triggered by the user using the mainloop() method. Now, let’s create the other methods that we’ll use in this constructor.

To display the title

def display_title(self):
    """To display title"""

    title = Label(self.window, text="iQuiz Application",
                      width=50, bg="green", fg="white", font=("ariel", 20, "bold"))
    title.place(x=0, y=2)

To display a title, we have created a Label widget on the main window. We set its width, bg, fg and font properties and it looks something like this:

Screenshot-2021-12-11-011507

To display a question

As we know, we have already created a canvas for the question text. Since the question_no is initialized with 0 in the QuizBrain class, we can get the questions using the next_question() method:

def display_question(self):
    """To display the question"""

    q_text = self.quiz.next_question()
    self.canvas.itemconfig(self.question_text, text=q_text)

Using the itemconfig() method in the Canvas class, we can add question text dynamically.

To create the radio buttons

Since the options will be four radio buttons, we’ll be using the RadioButton class from the Tkinter module.

def radio_buttons(self):
        """To create four options (radio buttons)"""
	# initialize the list with an empty list of options
    choice_list = []

    # position of the first option
    y_pos = 220

    # adding the options to the list
    while len(choice_list) < 4:

        # setting the radio button properties
        radio_btn = Radiobutton(self.window, text="", variable=self.user_answer, value='', font=("ariel", 14))

        # adding the button to the list
        choice_list.append(radio_btn)

        # placing the button
        radio_btn.place(x=200, y=y_pos)

        # incrementing the y-axis position by 40
        y_pos += 40

    # return the radio buttons
    return choice_list

First we created a choice_list list. We set the y-position of the first choice as 220. Using a while loop, we created four instances of the RadioButton class on the main window. Notice the variable attribute set as user_answer that we created earlier.

We’ll append these radio buttons in the choice_list and place them at a distance of 40 units in the y-axis. We then return the choice_list.

To display options

We’ll use this method to set the text and value attribute of each radio button.

def display_options(self):
    """To display four options"""

    val = 0

    # deselecting the options
    self.user_answer.set(None)

    # looping over the options to be displayed for the
    # text of the radio buttons.
    for option in self.quiz.current_question.choices:
        self.opts[val]['text'] = option
        self.opts[val]['value'] = option
        val += 1

We first set the user_answer to None. Then we iterate over the choices for the current_question and set the two properties one after another for each option.

To display buttons

As you can see, we’ve two buttons – Next and Quit.

Screenshot-2021-12-11-013502

We’ll use the Next button to move to the next question (if any). And we’ll use the Quit button to quit the quiz and destroy the window immediately.

We use the Button class from the Tkinter module to create them. The functionality for these buttons is added in the command attribute.

For the Next button, we’ll be creating a separate method right after this section. For the Quit button, we just destroy the main window.

Next button functionality

def next_btn(self):
    """To show feedback for each answer and keep checking for more questions"""

    # Check if the answer is correct
    if self.quiz.check_answer(self.user_answer.get()):
        self.feedback["fg"] = "green"
        self.feedback["text"] = 'Correct answer! U0001F44D'
    else:
        self.feedback['fg'] = 'red'
        self.feedback['text'] = ('u274E Oops! n'
                                     f'The right answer is: {self.quiz.current_question.correct_answer}')

    if self.quiz.has_more_questions():
        # Moves to next to display next question and its options
        self.display_question()
        self.display_options()
    else:
        # if no more questions, then it displays the score
        self.display_result()

        # destroys the self.window
        self.window.destroy()

The next button has to do a lot of things.

First of all, it checks whether the answer selected by the user is correct or not using the check_answer method. It shows the feedback accordingly.

Next, it checks if the quiz has more questions or not. If there are more questions, it calls the display_question and display_options methods again. If there are no questions left, it calls the display_result method to show the result and then destroys the main window.

To display the results

At the end of the quiz, we need to show the results to the user like this:

Screenshot-2021-12-11-014531

Here, as you can see, we are showing the score percentage based on correct answers and wrong answers.

def display_result(self):
    """To display the result using messagebox"""
    correct, wrong, score_percent = self.quiz.get_score()

    correct = f"Correct: {correct}"
    wrong = f"Wrong: {wrong}"

    # calculates the percentage of correct answers
    result = f"Score: {score_percent}%"

    # Shows a message box to display the result
    messagebox.showinfo("Result", f"{result}n{correct}n{wrong}")

We use the get_score method to get the computations and then we use the showinfo method from the messagebox class to show such a popup message.

Full code for quiz_ui.py

from tkinter import Tk, Canvas, StringVar, Label, Radiobutton, Button, messagebox
from quiz_brain import QuizBrain

THEME_COLOR = "#375362"


class QuizInterface:

    def __init__(self, quiz_brain: QuizBrain) -> None:
        self.quiz = quiz_brain
        self.window = Tk()
        self.window.title("iQuiz App")
        self.window.geometry("850x530")

        # Display Title
        self.display_title()

        # Create a canvas for question text, and dsiplay question
        self.canvas = Canvas(width=800, height=250)
        self.question_text = self.canvas.create_text(400, 125,
                                                     text="Question here",
                                                     width=680,
                                                     fill=THEME_COLOR,
                                                     font=(
                                                         'Ariel', 15, 'italic')
                                                     )
        self.canvas.grid(row=2, column=0, columnspan=2, pady=50)
        self.display_question()

        # Declare a StringVar to store user's answer
        self.user_answer = StringVar()

        # Display four options (radio buttons)
        self.opts = self.radio_buttons()
        self.display_options()

        # To show whether the answer is right or wrong
        self.feedback = Label(self.window, pady=10, font=("ariel", 15, "bold"))
        self.feedback.place(x=300, y=380)

        # Next and Quit Button
        self.buttons()

        # Mainloop
        self.window.mainloop()

    def display_title(self):
        """To display title"""

        # Title
        title = Label(self.window, text="iQuiz Application",
                      width=50, bg="green", fg="white", font=("ariel", 20, "bold"))

        # place of the title
        title.place(x=0, y=2)

    def display_question(self):
        """To display the question"""

        q_text = self.quiz.next_question()
        self.canvas.itemconfig(self.question_text, text=q_text)

    def radio_buttons(self):
        """To create four options (radio buttons)"""

        # initialize the list with an empty list of options
        choice_list = []

        # position of the first option
        y_pos = 220

        # adding the options to the list
        while len(choice_list) < 4:

            # setting the radio button properties
            radio_btn = Radiobutton(self.window, text="", variable=self.user_answer,
                                    value='', font=("ariel", 14))

            # adding the button to the list
            choice_list.append(radio_btn)

            # placing the button
            radio_btn.place(x=200, y=y_pos)

            # incrementing the y-axis position by 40
            y_pos += 40

        # return the radio buttons
        return choice_list

    def display_options(self):
        """To display four options"""

        val = 0

        # deselecting the options
        self.user_answer.set(None)

        # looping over the options to be displayed for the
        # text of the radio buttons.
        for option in self.quiz.current_question.choices:
            self.opts[val]['text'] = option
            self.opts[val]['value'] = option
            val += 1

    def next_btn(self):
        """To show feedback for each answer and keep checking for more questions"""

        # Check if the answer is correct
        if self.quiz.check_answer(self.user_answer.get()):
            self.feedback["fg"] = "green"
            self.feedback["text"] = 'Correct answer! U0001F44D'
        else:
            self.feedback['fg'] = 'red'
            self.feedback['text'] = ('u274E Oops! n'
                                     f'The right answer is: {self.quiz.current_question.correct_answer}')

        if self.quiz.has_more_questions():
            # Moves to next to display next question and its options
            self.display_question()
            self.display_options()
        else:
            # if no more questions, then it displays the score
            self.display_result()

            # destroys the self.window
            self.window.destroy()

    def buttons(self):
        """To show next button and quit button"""

        # The first button is the Next button to move to the
        # next Question
        next_button = Button(self.window, text="Next", command=self.next_btn,
                             width=10, bg="green", fg="white", font=("ariel", 16, "bold"))

        # palcing the button on the screen
        next_button.place(x=350, y=460)

        # This is the second button which is used to Quit the self.window
        quit_button = Button(self.window, text="Quit", command=self.window.destroy,
                             width=5, bg="red", fg="white", font=("ariel", 16, " bold"))

        # placing the Quit button on the screen
        quit_button.place(x=700, y=50)

    def display_result(self):
        """To display the result using messagebox"""
        correct, wrong, score_percent = self.quiz.get_score()

        correct = f"Correct: {correct}"
        wrong = f"Wrong: {wrong}"

        # calculates the percentage of correct answers
        result = f"Score: {score_percent}%"

        # Shows a message box to display the result
        messagebox.showinfo("Result", f"{result}n{correct}n{wrong}")

How to Put Everything Together

Since all the components are ready to be integrated together, let’s create a main.py file and add the following content there:

from question_model import Question
from quiz_data import question_data
from quiz_brain import QuizBrain
from quiz_ui import QuizInterface
from random import shuffle
import html

question_bank = []
for question in question_data:
    choices = []
    question_text = html.unescape(question["question"])
    correct_answer = html.unescape(question["correct_answer"])
    incorrect_answers = question["incorrect_answers"]
    for ans in incorrect_answers:
        choices.append(html.unescape(ans))
    choices.append(correct_answer)
    shuffle(choices)
    new_question = Question(question_text, correct_answer, choices)
    question_bank.append(new_question)


quiz = QuizBrain(question_bank)

quiz_ui = QuizInterface(quiz)


print("You've completed the quiz")
print(f"Your final score was: {quiz.score}/{quiz.question_no}")

We first imported all the classes from the different files we created above. In addition to that, we also need the shuffle method from the random module and the html module.

We have a list called question_bank. We are iterating over the question_data that we receive from the quiz_data.py file. If you see the sample response, you will find some text such as 'Dragon&#039. These need to be unescaped using the html.unescape method.

We have a choices list that will contain the correct answer as well as the incorrect answers. The list will be shuffled using the shuffle method from the random module.

After shuffling, we create a question using the Question model from quiz_model.py file and append it the question_bank list.

Next, we’re creating an object called quiz of the QuizBrain class which requires a list of questions. So, we’re passing the question_bank to it.

After that, we’re creating an object quiz_ui of the QuizInterface class which requires an object of the QuizBrain class, so we have passed the newly created quiz object to it.

Now that everything is ready, we are ready to run the application.

$ python main.py

Conclusion

Congrats on making it to the end! This was a basic tutorial on how you can build a GUI Quiz application using Tkinter. You can add more features and make the UI more attractive if you want to.

Here’s the code repository: https://github.com/ashutoshkrris/GUI-Quiz-Tkinter

For Windows, you can download the executable application from here.



Learn to code for free. freeCodeCamp’s open source curriculum has helped more than 40,000 people get jobs as developers. Get started

How to Build a GUI Quiz App Using Tkinter and Open Trivia DB

In this article, we’ll learn to build a Graphical User Interface (GUI) Quiz Application using the Tkinter Python built-in module.

The task is to ask multiple-choice questions, collect user answers, and finally display the results.

Before coding the GUI, we’ll first see how to fetch multiple-choice questions, their correct answers, and the choices from the Open Trivia DB API.

The Open Trivia Database provides a completely free JSON API that you can use in your programming projects. Use of this API does not require an API Key. To make the task more interesting, we’ll also randomize the order of choices.

Watch this video to see what we are building:

We’ll use the following modules and concepts in this project:

  • tkinter is a standard GUI library for Python using which we can build desktop apps. This is the base of our project and we’ll use it to create the User Interface of the application.
  • random module implements pseudo-random number generators for various distributions. This module will help us shuffle the options for the questions.
  • requests library allows us to send HTTP/1.1 requests extremely easily. We’ll need the library to fetch questions from the Open Trivia DB.
  • Python Classes are a blueprint for creating objects. Objects are real-world entities. During the entire project development, we’ll be separating our different functionalities into different classes and methods.

Workflow of the Project

The basic workflow of the application will go like this:

  1. We’ll fetch questions from the Open Trivia DB API.
  2. For each fetched question, we’ll create a different object using a Question class. All these Question objects will be appended to a question_bank list.
  3. This question_bank will be passed to the brain of the application, QuizBrain and a quiz object will be created. This class will be responsible for checking if there are more questions, for getting the next question, calculating the score, and so on.
  4. Finally, this quiz object will be passed to the QuizInterface class, and the user will be able to interact with it.

Alright, let’s get started!

How to Fetch Questions from the Open Trivia DB API

As we discussed above, we’ll be using the Open Trivia DB API to get the questions. Head over to their API, select the number of questions you want, along with the categories and difficulty.

The question type should be Multiple Choice and the encoding should be Default Encoding. Click on Generate API URL and you’ll get an API URL.

Here’s a sample API URL: https://opentdb.com/api.php?amount=10&type=multiple

For fetching the questions, we’ll be using the requests module. You can install it like this:

$ pip install requests

Let’s create a Python file quiz_data.py to fetch the quiz questions and answers using the API URL generated above.

import requests

parameters = {
    "amount": 10,
    "type": "multiple"
}

response = requests.get(url="https://opentdb.com/api.php", params=parameters)
question_data = response.json()["results"]

In the above script, instead of directly adding the amount and type parameters in the URL, we have created a parameters dictionary and added the respective values.

After that, we’re making a GET request using the requests library on the Open Trivia DB API URL. A sample JSON response looks like this:

{
  "response_code": 0,
  "results": [
    {
      "category": "Entertainment: Video Games",
      "type": "multiple",
      "difficulty": "hard",
      "question": "What was the name of the hero in the 80s animated video game 'Dragon's Lair'?",
      "correct_answer": "Dirk the Daring",
      "incorrect_answers": ["Arthur", "Sir Toby Belch", "Guy of Gisbourne"]
    },
    {
      "category": "Entertainment: Video Games",
      "type": "multiple",
      "difficulty": "medium",
      "question": "Which of these game franchises were made by Namco?",
      "correct_answer": "Tekken",
      "incorrect_answers": ["Street Fighter", "Mortal Kombat", "Dragon Quest"]
    }
  ]
}

The JSON data contains a dictionary with two keys: response_code and results. The response_code tells developers what the API is doing. The results is a list we are interested in. So, we have stored the value of results in a variable called question_data.

How to Create the Question Model

Question model is nothing but a Python class with three attributes – question_text, correct_answer and choices.

question_text is the question, correct_answer is the correct answer for that question, and choices is a list of options for that question.

Let’s create a question_model.py file and create the class in it:

class Question:
    def __init__(self, question: str, correct_answer: str, choices: list):
        self.question_text = question
        self.correct_answer = correct_answer
        self.choices = choices

How to Create the Quiz Brain

The QuizBrain, as the name suggests, is the brain of the application. Let’s create a quiz_brain.py file and add the following code there:

class QuizBrain:

    def __init__(self, questions):
        self.question_no = 0
        self.score = 0
        self.questions = questions
        self.current_question = None

    def has_more_questions(self):
        """To check if the quiz has more questions"""
        
        return self.question_no < len(self.questions)

    def next_question(self):
        """Get the next question by incrementing the question number"""
        
        self.current_question = self.questions[self.question_no]
        self.question_no += 1
        q_text = self.current_question.question_text
        return f"Q.{self.question_no}: {q_text}"

    def check_answer(self, user_answer):
        """Check the user's answer against the correct answer and maintain the score"""
        
        correct_answer = self.current_question.correct_answer
        if user_answer.lower() == correct_answer.lower():
            self.score += 1
            return True
        else:
            return False

    def get_score(self):
        """Get the number of correct answers, wrong answers, and score percentage."""
        
        wrong = self.question_no - self.score
        score_percent = int(self.score / self.question_no * 100)
        return (self.score, wrong, score_percent)

The QuizBrain class takes questions, a list of questions. Additionally, the question_no and score attributes are initialized with 0 and the current_question is set to None initially.

The first method has_more_questions() checks whether the quiz has more questions or not.

The next method next_question() gets the question from the questions list at index question_no and then increments the question_no attribute and returns a formatted question.

The check_answer() method takes user_answer as an argument and checks whether the user’s answer is correct or not. It also maintains the score and returns boolean values.

The last method get_score() returns the number of correct answers, wrong answers, and score percentage.

How to Build the Quiz UI

Let’s move on to the next part where we’ll create the user interface of the application. Create a quiz_ui.py file for this section, and add the following code.

class QuizInterface:

    def __init__(self, quiz_brain: QuizBrain) -> None:
        self.quiz = quiz_brain
        self.window = Tk()
        self.window.title("iQuiz App")
        self.window.geometry("850x530")

        # Display Title
        self.display_title()

        # Creating a canvas for question text, and dsiplay question
        self.canvas = Canvas(width=800, height=250)
        self.question_text = self.canvas.create_text(400, 125,
                                                     text="Question here",
                                                     width=680,
                                                     fill=THEME_COLOR,
                                                     font=(
                                                         'Ariel', 15, 'italic')
                                                     )
        self.canvas.grid(row=2, column=0, columnspan=2, pady=50)
        self.display_question()

        # Declare a StringVar to store user's answer
        self.user_answer = StringVar()

        # Display four options(radio buttons)
        self.opts = self.radio_buttons()
        self.display_options()

        # To show whether the answer is correct or wrong
        self.feedback = Label(self.window, pady=10, font=("ariel", 15, "bold"))
        self.feedback.place(x=300, y=380)

        # Next and Quit Button
        self.buttons()

        # Mainloop
        self.window.mainloop()

In the above code, we’ve created a QuizInterface class with a constructor. In Python, the __init__() method is called constructor and is called automatically whenever an object of that class is created.

As discussed in the workflow, the QuizInterface class takes an argument of type QuizBrain. So, within the constructor, we have passed that as quiz_brain.

The first thing we do in Tkinter is create a window using the Tk class. You can set the title and the geometry using the title() and geometry() methods respectively.

Next we called few methods which we’ll be creating next. Apart from that, we’ve created a canvas using the Canvas class where our questions will be placed. Canvas is a rectangular area where we can place text, graphics, widgets, and so on.

Inside the canvas, we added a sample text for now using the create_text() method. We then declared a StringVar variable called user_answer to store the user’s answer in String type.

Next we created a feedback label to show whether the answer is right or wrong using the Label widget. This widget implements a display box where we can place text or images. You can update the text displayed by this widget at any time you want.

At the very end, we enter the main event loop to take action against each event triggered by the user using the mainloop() method. Now, let’s create the other methods that we’ll use in this constructor.

To display the title

def display_title(self):
    """To display title"""

    title = Label(self.window, text="iQuiz Application",
                      width=50, bg="green", fg="white", font=("ariel", 20, "bold"))
    title.place(x=0, y=2)

To display a title, we have created a Label widget on the main window. We set its width, bg, fg and font properties and it looks something like this:

Screenshot-2021-12-11-011507

To display a question

As we know, we have already created a canvas for the question text. Since the question_no is initialized with 0 in the QuizBrain class, we can get the questions using the next_question() method:

def display_question(self):
    """To display the question"""

    q_text = self.quiz.next_question()
    self.canvas.itemconfig(self.question_text, text=q_text)

Using the itemconfig() method in the Canvas class, we can add question text dynamically.

To create the radio buttons

Since the options will be four radio buttons, we’ll be using the RadioButton class from the Tkinter module.

def radio_buttons(self):
        """To create four options (radio buttons)"""
	# initialize the list with an empty list of options
    choice_list = []

    # position of the first option
    y_pos = 220

    # adding the options to the list
    while len(choice_list) < 4:

        # setting the radio button properties
        radio_btn = Radiobutton(self.window, text="", variable=self.user_answer, value='', font=("ariel", 14))

        # adding the button to the list
        choice_list.append(radio_btn)

        # placing the button
        radio_btn.place(x=200, y=y_pos)

        # incrementing the y-axis position by 40
        y_pos += 40

    # return the radio buttons
    return choice_list

First we created a choice_list list. We set the y-position of the first choice as 220. Using a while loop, we created four instances of the RadioButton class on the main window. Notice the variable attribute set as user_answer that we created earlier.

We’ll append these radio buttons in the choice_list and place them at a distance of 40 units in the y-axis. We then return the choice_list.

To display options

We’ll use this method to set the text and value attribute of each radio button.

def display_options(self):
    """To display four options"""

    val = 0

    # deselecting the options
    self.user_answer.set(None)

    # looping over the options to be displayed for the
    # text of the radio buttons.
    for option in self.quiz.current_question.choices:
        self.opts[val]['text'] = option
        self.opts[val]['value'] = option
        val += 1

We first set the user_answer to None. Then we iterate over the choices for the current_question and set the two properties one after another for each option.

To display buttons

As you can see, we’ve two buttons – Next and Quit.

Screenshot-2021-12-11-013502

We’ll use the Next button to move to the next question (if any). And we’ll use the Quit button to quit the quiz and destroy the window immediately.

We use the Button class from the Tkinter module to create them. The functionality for these buttons is added in the command attribute.

For the Next button, we’ll be creating a separate method right after this section. For the Quit button, we just destroy the main window.

Next button functionality

def next_btn(self):
    """To show feedback for each answer and keep checking for more questions"""

    # Check if the answer is correct
    if self.quiz.check_answer(self.user_answer.get()):
        self.feedback["fg"] = "green"
        self.feedback["text"] = 'Correct answer! U0001F44D'
    else:
        self.feedback['fg'] = 'red'
        self.feedback['text'] = ('u274E Oops! n'
                                     f'The right answer is: {self.quiz.current_question.correct_answer}')

    if self.quiz.has_more_questions():
        # Moves to next to display next question and its options
        self.display_question()
        self.display_options()
    else:
        # if no more questions, then it displays the score
        self.display_result()

        # destroys the self.window
        self.window.destroy()

The next button has to do a lot of things.

First of all, it checks whether the answer selected by the user is correct or not using the check_answer method. It shows the feedback accordingly.

Next, it checks if the quiz has more questions or not. If there are more questions, it calls the display_question and display_options methods again. If there are no questions left, it calls the display_result method to show the result and then destroys the main window.

To display the results

At the end of the quiz, we need to show the results to the user like this:

Screenshot-2021-12-11-014531

Here, as you can see, we are showing the score percentage based on correct answers and wrong answers.

def display_result(self):
    """To display the result using messagebox"""
    correct, wrong, score_percent = self.quiz.get_score()

    correct = f"Correct: {correct}"
    wrong = f"Wrong: {wrong}"

    # calculates the percentage of correct answers
    result = f"Score: {score_percent}%"

    # Shows a message box to display the result
    messagebox.showinfo("Result", f"{result}n{correct}n{wrong}")

We use the get_score method to get the computations and then we use the showinfo method from the messagebox class to show such a popup message.

Full code for quiz_ui.py

from tkinter import Tk, Canvas, StringVar, Label, Radiobutton, Button, messagebox
from quiz_brain import QuizBrain

THEME_COLOR = "#375362"


class QuizInterface:

    def __init__(self, quiz_brain: QuizBrain) -> None:
        self.quiz = quiz_brain
        self.window = Tk()
        self.window.title("iQuiz App")
        self.window.geometry("850x530")

        # Display Title
        self.display_title()

        # Create a canvas for question text, and dsiplay question
        self.canvas = Canvas(width=800, height=250)
        self.question_text = self.canvas.create_text(400, 125,
                                                     text="Question here",
                                                     width=680,
                                                     fill=THEME_COLOR,
                                                     font=(
                                                         'Ariel', 15, 'italic')
                                                     )
        self.canvas.grid(row=2, column=0, columnspan=2, pady=50)
        self.display_question()

        # Declare a StringVar to store user's answer
        self.user_answer = StringVar()

        # Display four options (radio buttons)
        self.opts = self.radio_buttons()
        self.display_options()

        # To show whether the answer is right or wrong
        self.feedback = Label(self.window, pady=10, font=("ariel", 15, "bold"))
        self.feedback.place(x=300, y=380)

        # Next and Quit Button
        self.buttons()

        # Mainloop
        self.window.mainloop()

    def display_title(self):
        """To display title"""

        # Title
        title = Label(self.window, text="iQuiz Application",
                      width=50, bg="green", fg="white", font=("ariel", 20, "bold"))

        # place of the title
        title.place(x=0, y=2)

    def display_question(self):
        """To display the question"""

        q_text = self.quiz.next_question()
        self.canvas.itemconfig(self.question_text, text=q_text)

    def radio_buttons(self):
        """To create four options (radio buttons)"""

        # initialize the list with an empty list of options
        choice_list = []

        # position of the first option
        y_pos = 220

        # adding the options to the list
        while len(choice_list) < 4:

            # setting the radio button properties
            radio_btn = Radiobutton(self.window, text="", variable=self.user_answer,
                                    value='', font=("ariel", 14))

            # adding the button to the list
            choice_list.append(radio_btn)

            # placing the button
            radio_btn.place(x=200, y=y_pos)

            # incrementing the y-axis position by 40
            y_pos += 40

        # return the radio buttons
        return choice_list

    def display_options(self):
        """To display four options"""

        val = 0

        # deselecting the options
        self.user_answer.set(None)

        # looping over the options to be displayed for the
        # text of the radio buttons.
        for option in self.quiz.current_question.choices:
            self.opts[val]['text'] = option
            self.opts[val]['value'] = option
            val += 1

    def next_btn(self):
        """To show feedback for each answer and keep checking for more questions"""

        # Check if the answer is correct
        if self.quiz.check_answer(self.user_answer.get()):
            self.feedback["fg"] = "green"
            self.feedback["text"] = 'Correct answer! U0001F44D'
        else:
            self.feedback['fg'] = 'red'
            self.feedback['text'] = ('u274E Oops! n'
                                     f'The right answer is: {self.quiz.current_question.correct_answer}')

        if self.quiz.has_more_questions():
            # Moves to next to display next question and its options
            self.display_question()
            self.display_options()
        else:
            # if no more questions, then it displays the score
            self.display_result()

            # destroys the self.window
            self.window.destroy()

    def buttons(self):
        """To show next button and quit button"""

        # The first button is the Next button to move to the
        # next Question
        next_button = Button(self.window, text="Next", command=self.next_btn,
                             width=10, bg="green", fg="white", font=("ariel", 16, "bold"))

        # palcing the button on the screen
        next_button.place(x=350, y=460)

        # This is the second button which is used to Quit the self.window
        quit_button = Button(self.window, text="Quit", command=self.window.destroy,
                             width=5, bg="red", fg="white", font=("ariel", 16, " bold"))

        # placing the Quit button on the screen
        quit_button.place(x=700, y=50)

    def display_result(self):
        """To display the result using messagebox"""
        correct, wrong, score_percent = self.quiz.get_score()

        correct = f"Correct: {correct}"
        wrong = f"Wrong: {wrong}"

        # calculates the percentage of correct answers
        result = f"Score: {score_percent}%"

        # Shows a message box to display the result
        messagebox.showinfo("Result", f"{result}n{correct}n{wrong}")

How to Put Everything Together

Since all the components are ready to be integrated together, let’s create a main.py file and add the following content there:

from question_model import Question
from quiz_data import question_data
from quiz_brain import QuizBrain
from quiz_ui import QuizInterface
from random import shuffle
import html

question_bank = []
for question in question_data:
    choices = []
    question_text = html.unescape(question["question"])
    correct_answer = html.unescape(question["correct_answer"])
    incorrect_answers = question["incorrect_answers"]
    for ans in incorrect_answers:
        choices.append(html.unescape(ans))
    choices.append(correct_answer)
    shuffle(choices)
    new_question = Question(question_text, correct_answer, choices)
    question_bank.append(new_question)


quiz = QuizBrain(question_bank)

quiz_ui = QuizInterface(quiz)


print("You've completed the quiz")
print(f"Your final score was: {quiz.score}/{quiz.question_no}")

We first imported all the classes from the different files we created above. In addition to that, we also need the shuffle method from the random module and the html module.

We have a list called question_bank. We are iterating over the question_data that we receive from the quiz_data.py file. If you see the sample response, you will find some text such as 'Dragon&#039. These need to be unescaped using the html.unescape method.

We have a choices list that will contain the correct answer as well as the incorrect answers. The list will be shuffled using the shuffle method from the random module.

After shuffling, we create a question using the Question model from quiz_model.py file and append it the question_bank list.

Next, we’re creating an object called quiz of the QuizBrain class which requires a list of questions. So, we’re passing the question_bank to it.

After that, we’re creating an object quiz_ui of the QuizInterface class which requires an object of the QuizBrain class, so we have passed the newly created quiz object to it.

Now that everything is ready, we are ready to run the application.

$ python main.py

Conclusion

Congrats on making it to the end! This was a basic tutorial on how you can build a GUI Quiz application using Tkinter. You can add more features and make the UI more attractive if you want to.

Here’s the code repository: https://github.com/ashutoshkrris/GUI-Quiz-Tkinter

For Windows, you can download the executable application from here.



Learn to code for free. freeCodeCamp’s open source curriculum has helped more than 40,000 people get jobs as developers. Get started

Как определить, что работа выполнена качественно, все требования соблюдены и клиенты довольны? Можно, конечно, спросить, но это не путь Джедая…

Первым делом нужно продумать инструмент распространения, мне понравилась идея отправления ссылок на почту. Что бы еще добавить? Хочу визуализировать ответы в режиме реального времени!

Приступаем к реализации.

Ну, с опросником все понятно — гугл-форма, однозначно. Заполняем, подключаем к гугл диску, прописываем варианты ответов.

Самое интересное – это визуализация, но стандартный инструмент от google, с помощью которого можно построить графики — не позволяет осуществлять сложную предобработку данных. В качестве дополнительной фичи я хочу представить этот инструмент в качестве исполняемого файла своему руководству, что тоже недоступно при использовании стандартной визуализации в google forms.

Исходя из ограничений уже существующих инструментов, нужно поднять веб-сервис для отрисовки графиков. Просмотрев все возможные варианты решения проблемы я решила, что с этой задачей прекрасно справится библиотека plotly, которая представлена в виде платформы (на которой можно опубликовать свой интерактивный график). Преимущество этого инструмента заключается в возможности запуска инструмента в оффлайн. Документация по библиотеке, с примерами на python, есть на официальном сайте.

Для реализации задумки необходимо подключиться к API сервисам Google Drive и Plotly и получить свои API key:

tls.set_credentials_file(username=’youre username’, api_key=’youre api key’)
token = ‘youre token’
#переменная для дальнейшего обновления гистограммы
stream_ident = dict(token=token, maxpoints=60)
s = py.Stream(stream_id=token)

На Google диске нам необходимо подключиться к файлу, для этого стоит изучить этот Jupyter Notebook.

Пора переходить к самой интересной части. Так как я хочу видеть изменения графика в режиме реального времени, мне нужно использовать стриминг. При изучении plotly, я столкнулась с нехваткой актуального материала по этой платформе. Туториал, размещенный на официальном сайте меня подвел. При использовании параметров, которые прописаны в примерах, возвращаются ошибки, информирующие об отсутствии таковых. Мне помогло только чтение хелпа и интуиция.

Преобразуем скачанный excel файл в DataFrame, передаем значения для построения гистограммы в Bar (необходимо изначально обозначить размер передаваемых массивов).

command = list(data_set().keys()).sort()
points = [0]*len(list(data_set().keys()))
val=[0]*len(list(data_set().keys()))
clr=[0]*len(list(data_set().keys()))
#построение графиков
trace = go.Bar(x=command, y=points,
xaxis=’x2′,
yaxis=’y2′,
marker=dict(color=clr),
name=’Team’,
textfont=dict(
family=»Courier New, monospace»,
size=15,
color=»#003300″),
text=val,
textposition=»outside», #
stream=stream_id,)

data=[trace]
layout = go.Layout(
xaxis2=dict(
domain=[0,1],
anchor=’y2′,
categoryorder= ‘category ascending’,
tickfont=dict(size=16,
family=»Courier New, monospace»,
color=»#7f7f7f»),
automargin=True),
yaxis2=dict(
domain=[0,1],
rangemode=’nonnegative’,
autorange=True,
dtick=1,
anchor=’x2′,
categoryorder= ‘category ascending’,
tickfont=dict(size=16,
family=»Courier New, monospace»,
color=»#7f7f7f»)
))
fig = go.Figure(data=data, layout=layout)
url = py.plot(fig, filename=’simple-inset-stream’, auto_open=False)
webbrowser.open_new(url+’.embed’)

Обратите внимание, что в конце url-адреса нужно добавить строку ‘.embed’, это необходимо для корректной визуализации графика. Вариант без использования embed вас не порадует – подписи по оси х не отобразятся полностью (угол наклона = 90), также этот параметр растягивает график по размеру окна браузера и т.д.

Пора переходить к самой интересной части. Так как я хочу видеть изменения графика в режиме реального времени, мне нужно использовать стриминг в plotly. C интервалом в 1 секунду, я буду читать файл с диска и передавать новые массивы данных в plot. В ходе настройки графика я решила выделить самый большой столбец диаграммы другим цветом. Сделать это оказалось достаточно просто (в параметр color мы можем передать массив значений, сформированный в соответствии с необходимым условием).

if __name__ == ‘__main__’:
s.open()
while True:
data_set()
labels = list(data_set().keys())
values = [x-1 for x in list(data_set().values())]
clrs = [«#009933» if x == max(values) else ‘#66FFFF’ for x in values]
s.write(dict(x=labels, y=values,text=values, marker=dict(color=clrs), type=’bar’))
s.close()

Отправляем коллегам ссылки на голосование любым удобным способом, запускаем модуль, наслаждаемся.

В конечном счете получаем информативную сводку отзывов по направлениям и с удивлением узнаем зоны развития проекта.

Понравилась статья? Поделить с друзьями:
  • Как написать определение предела
  • Как написать опора
  • Как написать ополаскиватель
  • Как написать оплатить или уплатить как правильно
  • Как написать описательный текст