Before an exam, many students revise in pairs: one person reads out a question from a notebook, the other answers, and the first person checks the answer against a key written at the back of the page. It works, but it has a real cost — someone has to sit there for twenty minutes reading questions aloud, checking spellings, arguing about whether "Jaipur" and "jaipur" count as the same answer, and adding up the score by hand. In this project, you build a Python program that does that job for you: it asks the question, reads what you type, decides if you were right, keeps count, and tells you your final score — instantly and without getting tired or biased. This is not a toy example. It is the same basic pattern — ask, read, compare, count, report — behind every real quiz app you have used, from a classroom Kahoot round to the mock tests on exam-prep apps.
We will not jump straight to the final program. We will build it in four small, working stages, and at each stage we will run into a real bug that a beginner naturally hits — and fix it properly, the way a working programmer would, rather than pretending the bug never happens.
Stage 1: Can a program even ask one question?
Strip the problem down to its smallest possible version: one question, one check.
answer = input("What is the capital of India? ")
if answer == "Delhi":
print("Correct!")
else:
print("Wrong answer.")
Two things are doing real work here. input("...") prints the text inside the quotes to the screen, pauses the program, and waits for the student to type something and press Enter — whatever they typed comes back as a value, which we store in the variable answer. The if answer == "Delhi": line then compares that stored value against the exact text "Delhi" using ==, the equality-test operator (not to be confused with the single =, which means "store a value," a mix-up we will come back to). If the two sides match exactly, Python runs the indented line under if; otherwise it runs the indented line under else. Indentation is not decoration in Python — it is how the language knows which lines belong inside the if block and which do not. A misplaced space here is a genuine, common source of errors, not a style nitpick.
Run this in your head for a student who types Delhi exactly: answer becomes the string "Delhi", the comparison "Delhi" == "Delhi" is True, and the program prints Correct!. So far, so good.
Misconception 1: the computer does not know what you meant — it compares text exactly
Now try a student who knows the answer perfectly well but types delhi in lowercase, out of habit, the way you'd type a search query. The comparison becomes "delhi" == "Delhi". To a human these are obviously the same city. To Python they are two different strings — a capital "D" is a different character from a lowercase "d" — so the comparison is False, and a student who knew the right answer gets marked wrong. This is one of the most common early bugs in any program that checks user-typed text, and it is worth naming explicitly: equality between strings in Python is exact, character by character; it has no built-in idea of "close enough" or "means the same thing." A good quiz app should test knowledge of geography, not typing habits.
The fix is two small method calls chained onto the input:
answer = input("What is the capital of India? ")
if answer.strip().lower() == "delhi":
print("Correct!")
else:
print("Wrong answer.")
.lower() returns a new copy of the string with every letter converted to lowercase, so "Delhi" and "DELHI" and "delhi" all become "delhi" before comparison — which is why the correct answer on the right-hand side is now written in lowercase too, to match. .strip() removes any accidental leading or trailing spaces (very easy to type by mistake when pressing the spacebar before Enter). The order matters only in the sense that both must run before the comparison; chaining them as answer.strip().lower() does .strip() first, then .lower() on the result — either order gives the same final string here, but chaining is how you combine two transformations in one line.
Stage 2: one question is not a quiz — looping over many
A real quiz needs several questions asked one after another, with a running score. The most tempting first design is two separate lists, one holding the questions and one holding the matching answers, walked through together using their positions (indices):
questions = ["What is the capital of India?", "What is 6 x 7?"]
answers = ["delhi", "42"]
score = 0
for i in range(len(questions)):
print(questions[i])
user_answer = input("Your answer: ")
if user_answer.strip().lower() == answers[i]:
score += 1
print("Score:", score, "/", len(questions))
len(questions) is 2 (there are two questions), so range(len(questions)) produces the sequence 0, 1, and the for loop runs its body once with i = 0 and once with i = 1. On each pass, questions[i] and answers[i] fetch the question and answer sitting at that same position — index 0 in both lists, then index 1 in both lists. score += 1 is shorthand for score = score + 1: it adds one to whatever score currently holds.
Misconception 2: two lists that must always stay in sync are a hidden trap
This program works — but it is fragile in a way that is not obvious until it breaks. Suppose next month you want to add a new question about the Kosi river. If you insert it into the questions list but add its answer to the end of the answers list instead of the matching position, every question after the insertion point now silently pairs with the wrong answer. The program will not crash or show an error — it will simply grade correct answers as wrong (or, worse, wrong answers as correct) from that point onward, and nothing on screen tells you this happened. This class of bug — data that must be kept in sync across two separate structures purely by matching position — is a genuine and common design mistake, not just in student code but in real software. The fix is to stop relying on "the same position in two different lists" and instead store each question together with its own answer, as one unit.
Stage 3: pairing a question with its answer using a dictionary
A Python dictionary stores labelled values — pairs of a key and a value — inside curly braces, and you read a value back out using its key in square brackets. One question and its answer become a single dictionary:
q = {"question": "What is the capital of Rajasthan?", "answer": "jaipur"}
print(q["question"]) # What is the capital of Rajasthan?
print(q["answer"]) # jaipur
Now a whole quiz is simply a list of these little dictionaries — one list, where each item already carries both its question and its own answer glued together, so there is nothing left to fall out of sync:
questions = [
{"question": "What is the capital of Rajasthan?", "answer": "jaipur"},
{"question": "Which river is called the 'Sorrow of Bihar'?", "answer": "kosi"},
]
for q in questions:
print(q["question"])
Notice the loop changed shape: instead of for i in range(len(questions)) and then indexing with questions[i], we now write for q in questions, which hands us each dictionary directly, one at a time, with no index bookkeeping at all. Adding a new question later is now a single line — one new dictionary appended to one list — and it is structurally impossible for a question to end up paired with the wrong answer, because the pairing lives inside the item itself rather than depending on two lists staying aligned.
Stage 4: keeping score and reporting a result worth reading
A bare score out of 2 is not very informative once a quiz has ten or twenty questions. Two more pieces complete the picture: a percentage, and written feedback that changes depending on how well the student did. Percentage is a straightforward ratio scaled to 100: (score / total) * 100. The feedback uses a chain of conditions checked in order — if, then elif ("else if"), then a final else — where Python tests each condition top to bottom and runs the first one that is True, ignoring the rest.
The complete quiz application
questions = [
{"question": "What is the capital of Rajasthan?", "answer": "jaipur"},
{"question": "Which river is called the 'Sorrow of Bihar'?", "answer": "kosi"},
{"question": "What is 15 + 27?", "answer": "42"},
{"question": "Which planet is known as the Red Planet?", "answer": "mars"},
]
score = 0
total = len(questions)
for q in questions:
print(q["question"])
user_answer = input("Your answer: ")
correct_answer = q["answer"]
if user_answer.strip().lower() == correct_answer:
print("Correct!")
score = score + 1
else:
print("Wrong. The correct answer is:", correct_answer)
print()
percentage = (score / total) * 100
print("You scored", score, "out of", total)
print("Percentage:", percentage, "%")
if percentage >= 90:
print("Excellent! You are a quiz champion.")
elif percentage >= 60:
print("Good job! Keep practicing.")
else:
print("Keep studying - you'll do better next time.")
Every piece here has already been explained on its own — a list of dictionaries, a for loop that reads each one, .strip().lower() for a fair comparison, score = score + 1 to count correct answers, and an if/elif/else chain for feedback. What's new is only that they now work together as one program.
Tracing the program by hand
Reading code and knowing what it will actually print are two different skills — the second is what CBSE "trace the output" questions test, and it is also how you find your own bugs. Trace this exact program with a student who answers: Jaipur, Ganga, 42, mars.
- Before the loop:
score = 0,total = 4(the list has four dictionaries). - Question 1 ("capital of Rajasthan"): typed answer is
Jaipur.correct_answeris"jaipur"."Jaipur".strip().lower()gives"jaipur", which equals"jaipur"— match. PrintsCorrect!,scorebecomes1. - Question 2 ("Sorrow of Bihar"): typed answer is
Ganga.correct_answeris"kosi"."ganga" == "kosi"isFalse. PrintsWrong. The correct answer is: kosi.scorestays1. - Question 3 ("15 + 27"): typed answer is
42. Note thatinput()always returns a string, souser_answeris the text"42", not the number 42.correct_answeris also the string"42"(written that way in the dictionary), so"42" == "42"is a string comparison that happens to match. PrintsCorrect!,scorebecomes2. - Question 4 ("Red Planet"): typed answer is
mars, already lowercase, matches"mars". PrintsCorrect!,scorebecomes3. - After the loop:
percentage = (3 / 4) * 100. In Python, dividing with a single/always gives a decimal result even when it divides evenly, so3 / 4is0.75, and0.75 * 100is75.0. The program printsYou scored 3 out of 4andPercentage: 75.0 %. - Feedback chain: Python checks
75.0 >= 90first —False, so it skips toelif percentage >= 60—75.0 >= 60isTrue, so it printsGood job! Keep practicing.and, importantly, never even looks at theelsebranch, because one match in anelifchain stops the whole chain.
That question 3 step is worth pausing on, because it hides a subtle point students often get wrong: the match worked only because both sides happened to be written as text ("42" and "42"). If the dictionary had instead stored the answer as the number 42 and you tried to compare it directly against user_answer (a string), the comparison would always be False, no matter what digits the student typed — a string is never equal to a number in Python, even when they "look the same." This is why the quiz above deliberately keeps every answer, including numeric ones, as text: it sidesteps the string-versus-number mismatch entirely, at the cost of not being able to do arithmetic on the stored answer (which this quiz doesn't need to).
How the program flows: a visual map
The trace above followed one path through the code. The diagram below shows every path the program can take — the loop that repeats once per question, and the branch that repeats once per grading decision.
The green loop-back path on the left is the part beginners most often misunderstand: it is not a separate "restart the program" action, it simply carries control back up to the same decision diamond that started the loop, which then checks whether any questions remain. That single diamond is what makes a for loop a loop — the body runs, control returns to the check, and only when the list is exhausted does the "No" path finally let the program move on to computing the percentage.
Why a list of dictionaries, and not something else
It is worth being explicit about why this particular data structure was chosen, because "which structure should hold my data" is itself a core piece of algorithmic thinking, not an afterthought. A single dictionary models one question well because a question genuinely has named parts (a prompt, an answer) that belong together. A list models "many of these, in a fixed order" well, because a quiz should ask its questions in a predictable sequence and a list preserves the order you wrote them in. Combining them — a list of dictionaries — is the natural structure for "many records, each with named fields," which is exactly what a quiz is, and it is the same shape you will meet again in Class 9-10 CS problems and in any real database table, spreadsheet, or JSON API response: a table is nothing more than a list of dictionaries. Looping through total questions and doing a constant amount of work (one comparison, one print) per question costs time proportional to the number of questions — computer science calls this O(n) — which is as efficient as checking a quiz can possibly be, since you cannot grade a question you have not looked at.
Bugs students genuinely hit while building this
Three mistakes come up so often while building programs like this one that they deserve to be named directly, not left for you to discover by accident.
- Writing
if score = 0:instead ofif score == 0:. A single=is an assignment (store a value); Python does not even allow it inside anifcondition and will refuse to run the program with aSyntaxError, which is one of the friendlier bugs — it stops you immediately rather than silently misbehaving. - Forgetting the colon or the indentation after
for,if, orelse. Python decides which lines are "inside" a block purely by how far they are indented — there are no curly braces to fall back on. Mixing tabs and spaces, or indenting one line underifby one space more or less than the others, produces anIndentationError. This is a direct consequence of Python's design, not a quirk of this project, and CBSE papers frequently test exactly this by showing broken indentation and asking what goes wrong. - Comparing a typed answer to a number without converting types. As the trace above showed,
input()always returns a string. If a quiz question needs actual arithmetic on the answer (say, doubling a score the student typed in), you must explicitly convert it first withint(user_answer)— and if the student typed something that is not a whole number, that conversion raises aValueErrorand crashes the program unless you guard it. This quiz avoids the problem entirely by keeping every stored answer as text, which is a perfectly valid design choice as long as you are consistent about it, but it is a choice you should make on purpose, not by accident.
Practice: extend the program yourself
Work through these using the complete program above as your starting point. Each one changes exactly one part of the design.
- Add a fifth question about the ISRO mission "Chandrayaan-3" to the
questionslist. Trace by hand what the program prints if every one of the five answers is typed correctly — what wouldpercentageequal, and which feedback branch fires? - The program currently never tells the student which questions they got wrong once the quiz ends. Modify it to build a list called
missed, append a question's text to it whenever the answer check fails inside the loop, and print the whole list after the score. (Hint:missed = []before the loop,missed.append(q["question"])inside theelsebranch.) - What happens right now if a student presses Enter without typing anything? Trace it:
user_answerbecomes the empty string"". Does the program crash, or does it just mark the question wrong? Why? - Suppose you swapped the order of the two lines inside the
ifbranch so thatscore = score + 1ran beforeprint("Correct!"). Would the final score be different? Would anything at all change? Explain why the order of independent statements inside a block usually does not matter, while the order of theif/elif/elseconditions themselves does.
Summary
input()always returns a string, even when the student types digits; comparing that string against a stored answer only works cleanly if the stored answer is also kept as a string.- String equality in Python (
==) is exact and case-sensitive;.strip().lower()on both sides is what makes "Delhi", "delhi", and " DELHI " all count as the same answer. - Two separate lists kept in sync purely by matching index position are fragile — inserting into one and forgetting the other silently misaligns every entry after it, with no error message.
- A dictionary such as
{"question": ..., "answer": ...}keeps a question and its answer as one inseparable unit; a list of such dictionaries is the natural way to store "many records, each with named fields" — the same shape used by real databases and JSON data. for q in questions:hands you each dictionary directly, with no index bookkeeping, and is preferred overfor i in range(len(questions))whenever you do not actually need the position number.- An
if/elif/elsechain tests its conditions top to bottom and stops at the first one that isTrue— order the conditions from most restrictive (>= 90) to least, or a looser earlier condition will always fire first and hide the later ones. - The full program's control flow is one outer loop (once per question) containing one inner decision (correct or not), followed after the loop by a second, independent decision (which feedback tier) — two different loops of logic doing two different jobs, chained one after the other.
Think About It
Think about this: How would you explain project: python quiz application to a friend who has never seen a computer? What real-world analogy would you use? Imagine you had to build a system using these concepts — what would be your first step? Try this: before moving on, write down three things you learned and one question you still have.