Skip to content

Commit 1ac6baa

Browse files
authored
Merge pull request #262 from realpython/gahjelle-sbsp-python-quiz-application
Add code for quiz application
2 parents d62754b + 1990c17 commit 1ac6baa

File tree

10 files changed

+827
-0
lines changed

10 files changed

+827
-0
lines changed

python-quiz-application/README.md

Whitespace-only changes.
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
[python]
2+
label = "Python"
3+
4+
[[python.questions]]
5+
question = "When was the first known use of the word 'quiz'"
6+
answers = ["1781"]
7+
alternatives = ["1771", "1871", "1881"]
8+
9+
[[python.questions]]
10+
question = "Which built-in function can get information from the user"
11+
answers = ["input"]
12+
alternatives = ["get", "print", "write"]
13+
14+
[[python.questions]]
15+
question = "What's the purpose of the built-in zip() function"
16+
answers = ["To iterate over two or more sequences at the same time"]
17+
alternatives = [
18+
"To combine several strings into one",
19+
"To compress several files into one archive",
20+
"To get information from the user",
21+
]
22+
23+
[[python.questions]]
24+
question = "What does dict.get(key) return if key isn't found in dict"
25+
answers = ["None"]
26+
alternatives = ["key", "True", "False"]
27+
28+
[[python.questions]]
29+
question = "How do you iterate over both the indices and elements in an iterable"
30+
answers = ["enumerate(iterable)"]
31+
alternatives = [
32+
"enumerate(iterable, start=1)",
33+
"range(iterable)",
34+
"range(iterable, start=1)",
35+
]
36+
37+
[[python.questions]]
38+
question = "What's the official name of the := operator"
39+
answers = ["Assignment expression"]
40+
alternatives = [
41+
"Named expression",
42+
"Walrus operator",
43+
"Colon equals operator",
44+
]
45+
46+
[[python.questions]]
47+
question = "What's one effect of calling random.seed(42)"
48+
answers = ["The random numbers are reproducible."]
49+
alternatives = [
50+
"The random numbers are more random.",
51+
"The computer clock is reset.",
52+
"The first random number is always 42.",
53+
]
54+
55+
[[python.questions]]
56+
question = "Which version of Python is the first with TOML support built in"
57+
answers = ["3.11"]
58+
alternatives = ["3.9", "3.10", "3.12"]
59+
60+
[[python.questions]]
61+
question = "How can you run a Python script named quiz.py"
62+
answers = ["python quiz.py", "python -m quiz"]
63+
alternatives = ["python quiz", "python -m quiz.py"]
64+
hint = "One option uses the filename, and the other uses the module name."
65+
66+
[[python.questions]]
67+
question = "What's the name of the list-like data structure in TOML"
68+
answers = ["Array"]
69+
alternatives = ["List", "Sequence", "Set"]
70+
71+
[[python.questions]]
72+
question = "What's a PEP"
73+
answers = ["A Python Enhancement Proposal"]
74+
alternatives = [
75+
"A Pretty Exciting Policy",
76+
"A Preciously Evolved Python",
77+
"A Potentially Epic Prize",
78+
]
79+
hint = "PEPs are used to evolve Python."
80+
explanation = """
81+
Python Enhancement Proposals (PEPs) are design documents that provide
82+
information to the Python community. PEPs are used to propose new features
83+
for the Python language, to collect community input on an issue, and to
84+
document design decisions made about the language.
85+
"""
86+
87+
[[python.questions]]
88+
question = "How can you add a docstring to a function"
89+
answers = [
90+
"By writing a string literal as the first statement in the function",
91+
"By assigning a string to the function's .__doc__ attribute",
92+
]
93+
alternatives = [
94+
"By using the built-in @docstring decorator",
95+
"By returning a string from the function",
96+
]
97+
hint = "They are parsed from your code and stored on the function object."
98+
explanation = """
99+
Docstrings document functions and other Python objects. A docstring is a
100+
string literal that occurs as the first statement in a module, function,
101+
class, or method definition. Such a docstring becomes the .__doc__ special
102+
attribute of that object. See PEP 257 for more information.
103+
104+
There's no built-in @docstring decorator. Many functions naturally return
105+
strings. Such a feature can therefore not be used for docstrings.
106+
"""
107+
108+
[[python.questions]]
109+
question = "When was the first public version of Python released"
110+
answers = ["February 1991"]
111+
alternatives = ["January 1994", "October 2000", "December 2008"]
112+
hint = "The first public version was labeled version 0.9.0."
113+
explanation = """
114+
Guido van Rossum started work on Python in December 1989. He posted
115+
Python v0.9.0 to the alt.sources newsgroup in February 1991. Python
116+
reached version 1.0.0 in January 1994. The next major versions,
117+
Python 2.0 and Python 3.0, were released in October 2000 and December
118+
2008, respectively.
119+
"""
120+
121+
[capitals]
122+
label = "Capitals"
123+
124+
[[capitals.questions]]
125+
question = "What's the capital of Norway"
126+
answers = ["Oslo"]
127+
hint = "Lars Onsager, Jens Stoltenberg, Trygve Lie, and Børge Ousland."
128+
alternatives = ["Stockholm", "Copenhagen", "Helsinki", "Reykjavik"]
129+
explanation = """
130+
Oslo was founded as a city in the 11th century and established as a
131+
trading place. It became the capital of Norway in 1299. The city was
132+
destroyed by a fire in 1624 and rebuilt as Christiania, named in honor
133+
of the reigning king. The city was renamed back to Oslo in 1925.
134+
"""
135+
136+
[[capitals.questions]]
137+
question = "What's the state capital of Texas, USA"
138+
answers = ["Austin"]
139+
alternatives = ["Harrisburg", "Houston", "Galveston", "Columbia"]
140+
hint = "SciPy is held there each year."
141+
explanation = """
142+
Austin is named in honor of Stephen F. Austin. It was purpose-built to
143+
be the capital of Texas and was incorporated in December 1839. Houston,
144+
Harrisburg, Columbia, and Galveston are all earlier capitals of Texas.
145+
"""
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
# quiz.py
2+
3+
import pathlib
4+
import random
5+
from string import ascii_lowercase
6+
7+
try:
8+
import tomllib
9+
except ModuleNotFoundError:
10+
import tomli as tomllib
11+
12+
NUM_QUESTIONS_PER_QUIZ = 5
13+
QUESTIONS_PATH = pathlib.Path(__file__).parent / "questions.toml"
14+
15+
16+
def run_quiz():
17+
questions = prepare_questions(
18+
QUESTIONS_PATH, num_questions=NUM_QUESTIONS_PER_QUIZ
19+
)
20+
21+
num_correct = 0
22+
for num, question in enumerate(questions, start=1):
23+
print(f"\nQuestion {num}:")
24+
num_correct += ask_question(question)
25+
26+
print(f"\nYou got {num_correct} correct out of {num} questions")
27+
28+
29+
def prepare_questions(path, num_questions):
30+
topic_info = tomllib.loads(path.read_text())
31+
topics = {
32+
topic["label"]: topic["questions"] for topic in topic_info.values()
33+
}
34+
topic_label = get_answers(
35+
question="Which topic do you want to be quizzed about",
36+
alternatives=sorted(topics),
37+
)[0]
38+
39+
questions = topics[topic_label]
40+
num_questions = min(num_questions, len(questions))
41+
return random.sample(questions, k=num_questions)
42+
43+
44+
def ask_question(question):
45+
correct_answers = question["answers"]
46+
alternatives = question["answers"] + question["alternatives"]
47+
ordered_alternatives = random.sample(alternatives, k=len(alternatives))
48+
49+
answers = get_answers(
50+
question=question["question"],
51+
alternatives=ordered_alternatives,
52+
num_choices=len(correct_answers),
53+
hint=question.get("hint"),
54+
)
55+
if correct := (set(answers) == set(correct_answers)):
56+
print("⭐ Correct! ⭐")
57+
else:
58+
is_or_are = " is" if len(correct_answers) == 1 else "s are"
59+
print("\n- ".join([f"No, the answer{is_or_are}:"] + correct_answers))
60+
61+
if "explanation" in question:
62+
print(f"\nEXPLANATION:\n{question['explanation']}")
63+
64+
return 1 if correct else 0
65+
66+
67+
def get_answers(question, alternatives, num_choices=1, hint=None):
68+
print(f"{question}?")
69+
labeled_alternatives = dict(zip(ascii_lowercase, alternatives))
70+
if hint:
71+
labeled_alternatives["?"] = "Hint"
72+
73+
for label, alternative in labeled_alternatives.items():
74+
print(f" {label}) {alternative}")
75+
76+
while True:
77+
plural_s = "" if num_choices == 1 else f"s (choose {num_choices})"
78+
answer = input(f"\nChoice{plural_s}? ")
79+
answers = set(answer.replace(",", " ").split())
80+
81+
# Handle hints
82+
if hint and "?" in answers:
83+
print(f"\nHINT: {hint}")
84+
continue
85+
86+
# Handle invalid answers
87+
if len(answers) != num_choices:
88+
plural_s = "" if num_choices == 1 else "s, separated by comma"
89+
print(f"Please answer {num_choices} alternative{plural_s}")
90+
continue
91+
92+
if any(
93+
(invalid := answer) not in labeled_alternatives
94+
for answer in answers
95+
):
96+
print(
97+
f"{invalid!r} is not a valid choice. " # noqa
98+
f"Please use {', '.join(labeled_alternatives)}"
99+
)
100+
continue
101+
102+
return [labeled_alternatives[answer] for answer in answers]
103+
104+
105+
if __name__ == "__main__":
106+
run_quiz()
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
QUESTIONS = {
2+
"When was the first known use of the word 'quiz'": [
3+
"1781",
4+
"1771",
5+
"1871",
6+
"1881",
7+
],
8+
"Which built-in function can get information from the user": [
9+
"input",
10+
"get",
11+
"print",
12+
"write",
13+
],
14+
"Which keyword do you use to loop over a given list of elements": [
15+
"for",
16+
"while",
17+
"each",
18+
"loop",
19+
],
20+
"What's the purpose of the built-in zip() function": [
21+
"To iterate over two or more sequences at the same time",
22+
"To combine several strings into one",
23+
"To compress several files into one archive",
24+
"To get information from the user",
25+
],
26+
"What's the name of Python's sorting algorithm": [
27+
"Timsort",
28+
"Quicksort",
29+
"Merge sort",
30+
"Bubble sort",
31+
],
32+
}
33+
34+
for question, alternatives in QUESTIONS.items():
35+
correct_answer = alternatives[0]
36+
sorted_alternatives = sorted(alternatives)
37+
for label, alternative in enumerate(sorted_alternatives):
38+
print(f" {label}) {alternative}")
39+
40+
answer_label = int(input(f"{question}? "))
41+
answer = sorted_alternatives[answer_label]
42+
if answer == correct_answer:
43+
print("Correct!")
44+
else:
45+
print(f"The answer is {correct_answer!r}, not {answer!r}")
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import random
2+
from string import ascii_lowercase
3+
4+
NUM_QUESTIONS_PER_QUIZ = 5
5+
QUESTIONS = {
6+
"When was the first known use of the word 'quiz'": [
7+
"1781",
8+
"1771",
9+
"1871",
10+
"1881",
11+
],
12+
"Which built-in function can get information from the user": [
13+
"input",
14+
"get",
15+
"print",
16+
"write",
17+
],
18+
"Which keyword do you use to loop over a given list of elements": [
19+
"for",
20+
"while",
21+
"each",
22+
"loop",
23+
],
24+
"What's the purpose of the built-in zip() function": [
25+
"To iterate over two or more sequences at the same time",
26+
"To combine several strings into one",
27+
"To compress several files into one archive",
28+
"To get information from the user",
29+
],
30+
"What's the name of Python's sorting algorithm": [
31+
"Timsort",
32+
"Quicksort",
33+
"Merge sort",
34+
"Bubble sort",
35+
],
36+
"What does dict.get(key) return if key isn't found in dict": [
37+
"None",
38+
"key",
39+
"True",
40+
"False",
41+
],
42+
"How do you iterate over both indices and elements in an iterable": [
43+
"enumerate(iterable)",
44+
"enumerate(iterable, start=1)",
45+
"range(iterable)",
46+
"range(iterable, start=1)",
47+
],
48+
"What's the official name of the := operator": [
49+
"Assignment expression",
50+
"Named expression",
51+
"Walrus operator",
52+
"Colon equals operator",
53+
],
54+
"What's one effect of calling random.seed(42)": [
55+
"The random numbers are reproducible.",
56+
"The random numbers are more random.",
57+
"The computer clock is reset.",
58+
"The first random number is always 42.",
59+
],
60+
}
61+
62+
num_questions = min(NUM_QUESTIONS_PER_QUIZ, len(QUESTIONS))
63+
questions = random.sample(list(QUESTIONS.items()), k=num_questions)
64+
65+
num_correct = 0
66+
for num, (question, alternatives) in enumerate(questions, start=1):
67+
print(f"\nQuestion {num}:")
68+
print(f"{question}?")
69+
correct_answer = alternatives[0]
70+
labeled_alternatives = dict(
71+
zip(ascii_lowercase, random.sample(alternatives, k=len(alternatives)))
72+
)
73+
for label, alternative in labeled_alternatives.items():
74+
print(f" {label}) {alternative}")
75+
76+
while (answer_label := input("\nChoice? ")) not in labeled_alternatives:
77+
print(f"Please answer one of {', '.join(labeled_alternatives)}")
78+
79+
answer = labeled_alternatives[answer_label]
80+
if answer == correct_answer:
81+
num_correct += 1
82+
print("⭐ Correct! ⭐")
83+
else:
84+
print(f"The answer is {correct_answer!r}, not {answer!r}")
85+
86+
print(f"\nYou got {num_correct} correct out of {num} questions")

0 commit comments

Comments
 (0)