LESSON 45 · PRACTICE PROJECT26 min

Project: Flashcard Quiz

Grade flashcard answers, report a score, and review missed questions without hiding a length mismatch.

Example check: Worked and guided code samples are checked against CPython 3.13. Practice uses Pyodide 0.29.5 (Python 3.13) in your browser. How we check lessons

Video lesson: Project: Flashcard Quiz

The preview is stored on this site. YouTube loads only when you press Play. Watch on YouTube

Read the video transcript

Select a timestamp to watch that moment on YouTube.

  1. 00:00

    We are building a tiny Python flashcard quiz. Each card asks a question and stores the expected answer. A learner submits one response for each card, and the program reports a score plus the questions to review. That sounds like a short loop, but there is a subtle fairness problem. What if the deck has three cards and the learner supplies only two answers? If our loop quietly ignores the final card, it may show a flattering score for an incomplete attempt. We will deliberately run that broken version, inspect why it happens, and then make the one-response-per-card rule explicit. The browser lesson uses a list of responses rather than calling input.

  2. 00:42

    This makes every run repeatable: you can change one response, run the same deck again, and see exactly how the score changes. We will also accept harmless capitalization and surrounding spaces by normalizing both sides of each comparison. Finally, we will test an empty deck and show how to review missed prompts. Predict the first broken result before we run it: will Python warn us about the missing third response, or will the loop simply stop? This program ran without an exception. That is exactly why the mistake is easy to miss. There are three cards, but zip created only two pairs because the response list ended first.

  3. 01:23

    The loop graded those two correct answers and printed a perfect two out of two. If the report looked only at the score, a person might think the whole deck was completed. The third question, Exit a loop, was never evaluated. This is not a bug in zip; stopping at the shorter input is its documented behavior. The bug is our assumption that a pair would exist for every card without first checking it. A more honest quiz has to define the length contract. We could decide to count missing answers as wrong, but this lesson chooses a stricter rule: one response is required for every card, and any mismatch raises a clear ValueError.

  4. 02:04

    It should reject an extra response too, since we would not know which card that value belongs to. The important habit is to state the policy before scoring, not let an iterator quietly decide it for us. Now we have a useful failure instead of a misleading success. The length check detects that three cards and two responses cannot form a complete attempt. The ValueError makes the contract visible to the caller. In a website interface, we could catch this error and ask the learner to answer the remaining card. In the checked project, raising the error is enough to prove the function did not silently grade a partial deck. Notice that a matching length alone does not make responses correct; it only makes the grading pass complete.

  5. 02:50

    We will compare each paired response next. Raw string comparison is too strict for these simple keyword cards. A response containing spaces around DEF should match the expected answer def. We can strip surrounding whitespace and apply casefold to both values, producing the same comparison form. That normalization is suitable for these short exact-answer cards. It does not solve every possible quiz ambiguity: a question with multiple reasonable answers needs an explicit accepted-answer list or a different grading policy. For now, the criteria are clear enough to test. The function begins by enforcing equal lengths, so every card has one response.

  6. 03:34

    It then pairs cards and responses in their list order. We compare the normalized learner response with the normalized expected answer. On a match, correct increases by one. On a mismatch, we append the card's prompt to missed, preserving deck order. The first response has extra spaces and capital letters but matches def. The second response says list when the expected answer is dict, so Key-value collection goes into the review list. The last response matches break. The report shows two of three, and the caller prints the missed prompt. Returning a dictionary is useful here: the same grader can feed a command-line display, a browser page, or a future progress chart without embedding print statements in its core logic.

  7. 04:21

    We keep the original answer text in each card so the interface can later reveal it for study. This function does not automatically save progress or randomize questions; it is intentionally small enough that every branch can be observed and checked. Let us test behavior beyond the sample score. The first assertion checks that normalization works on the expected answer as well as on the learner response. The second checks an empty deck. The grader returns zero correct, zero total, and no missed prompts; it does not divide by zero because it returns counts rather than a percentage. If you later show a percentage, decide what zero cards means before writing the division.

  8. 05:05

    The final test passes an extra response to an empty deck and expects ValueError. It would be just as important to reject a missing response, as we saw in the diagnostic run. All three checks passed. On the companion lesson page, fill in the grading loop, run the sample, and select Check task. The checker exercises capitalization, missed-prompt order, empty data, and length mismatches, so a hard-coded two-out-of-three result will not pass. Complete the short quiz to mark the project done. As a next step, you could store a deck in JSON, shuffle its order with a fixed seed for reproducible practice, or track which prompts are repeatedly missed.

  9. 05:48

    Start with this transparent grader and build those features one at a time.

Understand the concept

A useful flashcard quiz gives the learner feedback that can be checked and repeated. Each card is a dictionary with prompt and answer fields. Instead of asking for interactive input(), the browser example receives a separate list of responses. The same cards and responses always produce the same result, making the project easy to debug and test.

The grading function checks that there is one response per card before comparing anything. zip normally stops at the shorter input, so without this check an unanswered card could disappear from the score. For matching, strip surrounding whitespace and apply casefold() to both the learner response and expected answer. That accepts def, DEF, and ' def ' as equivalent while retaining the original answer text for review.

Return a dictionary with correct, total, and missed. missed is a list of prompts answered incorrectly, in card order. A caller can print the score and show each missed prompt. An empty deck is valid and returns zero of zero with no misses; do not compute a percentage by dividing by total unless you handle zero. Later versions could randomize card order, store results, allow multiple accepted answers, and use a user interface for interactive responses.

  • A list of dictionaries models a deck
  • zip pairs each card with one response after a length check
  • strip and casefold normalize a comparison
  • A structured return value separates grading from display

See it step by step

Read the code, predict the output, then compare it with the result.

01. Pair a card with its response

PYTHON
cards = [{"prompt": "Function keyword?", "answer": "def"}]
responses = [" DEF "]
for card, response in zip(cards, responses):
    print(card["prompt"])
    print(response.strip().casefold())
EXPECTED OUTPUT
Function keyword?
def

The paired loop keeps each response beside its card. Normalization changes the comparison value, not the displayed prompt.

02. Grade and collect a missed prompt

PYTHON
cards = [{"prompt": "Keyword?", "answer": "def"}, {"prompt": "Loop exit?", "answer": "break"}]
responses = ["DEF", "continue"]
correct = 0
missed = []
for card, response in zip(cards, responses):
    if response.strip().casefold() == card["answer"].strip().casefold():
        correct += 1
    else:
        missed.append(card["prompt"])
print(correct)
print(missed)
EXPECTED OUTPUT
1
['Loop exit?']

Only the first answer matches. The second card's prompt goes into the review list.

03. Catch a missing response before zip

PYTHON
cards = ["A?", "B?"]
responses = ["yes"]
try:
    if len(cards) != len(responses):
        raise ValueError("One response is required for each card")
    print(list(zip(cards, responses)))
except ValueError as error:
    print(error)
EXPECTED OUTPUT
One response is required for each card

zip by itself would silently ignore the second card. The length check makes missing input visible.

A closer look

Follow the reasoning, inspect each result, then try the suggested changes in the console below.

01 / 03

Model a deck that can be graded repeatedly

Each flashcard stores a prompt and an expected answer under named dictionary keys. A list keeps the deck in a predictable order. Store learner responses in a second list in the same order. This project receives those responses as data rather than calling input(), which would be awkward in an automated browser exercise and harder to reproduce when debugging.

Normalize comparisons with strip().casefold() on both sides. strip removes surrounding whitespace; casefold creates a case-insensitive comparison form. Keep the original prompt and answer strings for display. Do not mutate the cards merely to decide whether a response is correct.

PYTHON
card = {"prompt": "Function keyword?", "answer": "def"}
response = " DEF "
print(card["prompt"])
print(response.strip().casefold())
print(response.strip().casefold() == card["answer"].strip().casefold())
EXPECTED OUTPUT
Function keyword?
def
True
Follow the reasoning
  1. The response's extra spaces and capital letters disappear only in the comparison form.
  2. The original prompt remains ready for review if the answer is wrong.
  3. Try an expected answer with surrounding spaces and confirm both sides are normalized.
02 / 03

Enforce one response per card before pairing

zip pairs the first card with the first response, the second with the second, and so on. Its default behavior stops when the shorter input ends. That is convenient in many programs but dangerous for a quiz: if a three-card deck has only two responses, the final card would silently disappear from the grading loop.

Compare lengths before zip and raise ValueError if they differ. This catches both missing and extra responses. The caller can show the error or ask the learner to finish the deck. This check also documents the function's contract: a full attempt has exactly one response for each card.

PYTHON
cards = [{"prompt": "A?", "answer": "a"}, {"prompt": "B?", "answer": "b"}]
responses = ["a"]
print(len(list(zip(cards, responses))))
try:
    if len(cards) != len(responses):
        raise ValueError("One response is required for each card")
except ValueError as error:
    print(error)
EXPECTED OUTPUT
1
One response is required for each card
Follow the reasoning
  1. zip would pair just one of the two cards.
  2. The explicit length check catches the incomplete attempt before grading.
  3. Add a second response and predict what the zip length becomes.
03 / 03

Return feedback and handle an empty deck

Rather than print inside the grader, return a report dictionary. correct counts matches, total records the number of cards, and missed holds prompts for incorrect responses in deck order. The caller can then format Score: 2/3 and a short review list. This makes the core function reusable from a website, a command-line program, or a later test.

An empty deck is a legitimate input: zero correct, zero total, and an empty missed list. No division occurs in the grader, so it does not raise ZeroDivisionError. If you later display a percentage, choose an explicit rule for zero cards. For a richer study tool, you might allow multiple accepted answers, track difficulty, and persist progress, but this first version keeps grading transparent.

PYTHON
def grade_flashcards(cards, responses):
    if len(cards) != len(responses):
        raise ValueError("One response is required for each card")
    correct = 0
    missed = []
    for card, response in zip(cards, responses):
        if response.strip().casefold() == card["answer"].strip().casefold():
            correct += 1
        else:
            missed.append(card["prompt"])
    return {"correct": correct, "total": len(cards), "missed": missed}

deck = [{"prompt": "Keyword?", "answer": "def"}, {"prompt": "Loop exit?", "answer": "break"}]
print(grade_flashcards(deck, ["DEF", "continue"]))
print(grade_flashcards([], []))
EXPECTED OUTPUT
{'correct': 1, 'total': 2, 'missed': ['Loop exit?']}
{'correct': 0, 'total': 0, 'missed': []}
Follow the reasoning
  1. The first normalized response matches and increments correct.
  2. The second response is wrong, so its prompt goes to missed.
  3. The empty run returns counts without attempting to divide by zero.

Try it in Python

Edit the example and run it. Python starts in your browser the first time you click Run.

Python console

Ready to run
Need input()? Add one value per line
Ctrl / ⌘ + Enter to run
OUTPUT
Your output appears here.
Your turnComplete grade_flashcards(cards, responses). Keep the length check. For each paired card and response, compare response.strip().casefold() to card['answer'].strip().casefold(). Increase correct for a match; otherwise append the card's prompt to missed. Return the prepared report dictionary. The sample must print Score: 2/3 and Review: Key-value collection?. An empty deck returns {'correct': 0, 'total': 0, 'missed': []}; missing or extra responses raise ValueError.
Need a hint?

Inside the loop, write if response.strip().casefold() == card['answer'].strip().casefold(): correct += 1; else: missed.append(card['prompt']). Do not change the length check before zip.

Complete the task and select Check task to verify your code.

    Quick quiz

    Three questions. You can change your answers and try again.

    01.What happens when zip receives two lists of different lengths?

    02.Why normalize both the response and the expected answer?

    03.What should an empty deck return?

    Typical mistakes

    Everyone meets these errors. See what causes them and how to fix them.

    Letting zip hide an unanswered card

    COMMON MISTAKE
    for card, response in zip(cards, responses):
        grade(card, response)
    FIX
    if len(cards) != len(responses):
        raise ValueError("One response is required for each card")
    for card, response in zip(cards, responses):
        ...

    What happens: With three cards and two responses, the third card is never graded.
    Check the one-to-one contract before pairing the lists.

    Normalizing only the learner response

    COMMON MISTAKE
    if response.strip().casefold() == card["answer"]:
    FIX
    if response.strip().casefold() == card["answer"].strip().casefold():

    What happens: An expected answer stored as DEF still rejects a normalized def response.
    Apply the same normalization to both strings before comparing them.

    Dividing by the number of cards without an empty-deck rule

    COMMON MISTAKE
    percent = correct / len(cards) * 100
    FIX
    percent = correct / len(cards) * 100 if cards else 0

    What happens: An empty deck raises ZeroDivisionError.
    The core grader returns counts, and any optional percentage display needs an explicit zero-card policy.

    Browse this lesson’s problem fixes

    Explore the runnable code

    The GitHub project includes setup instructions and source code you can run on your computer. Use this lesson for the guided examples, browser task, and quiz.

    View this project on GitHub

    Sources and further reading