EXERCISE 22: Add session-spanning user preference setting

Task: Add a menu option and extend your JSON structure to allow the user to save and modify their preferred number of flashcards per session. At the moment you simply have a list of dictionaries to store progress history.

Now you need to create an extra level of nesting so that you can include "max_cards", the preferred number of flashcards the user wishes to practice per session, and their "name". You might also like to name this file user.json rather than progress.json since it stores both the progress and user preference data. E.g. user.json

{
    "name": "User 1",
    "max_cards": 10, 
    "progress_history": [
    {
        "num_cards_practiced": 10,
        "score": 0.8
    },
    {
        "num_cards_practiced": 10,
        "score": 0.6
    },
    {
        "num_cards_practiced": 10,
        "score": 0.5
    }]
}

Regardless of whether you are saving the progress_history data or the value of max_cards, remember that you need to work with the entire JSON dictionary structure. Correspondingly, when you read from the file, you will need to extract the progress_history and max_cards elements from the dictionary before you can use the data associated them.

  1. Modify your previous code to load and save the user’s progress history so that it takes into account this new structure.

  2. Add code that attempts to initialise the values name and max_cards from user.json at the beginning of the program.

  3. If the user.json file is not found or the field "name" is not found in the dictionary structure, ask the user for their name (as you did previously).

  4. Write a function called set_max_cards(), which asks the user for their preferred number of cards per session, sets the max_cards variable, and then saves it to user.json. The function should:
    • Have a dictionary user_data which can either be empty or initialised by reading from the user.json.
    • Asks the user to enter their preferred number of cards to practice per session (you can reuse your existing code).
    • Saves the value entered by the user to the "max_cards" field of the user_data dictionary and then writes user_data (back) to user.json.
    • Ideally also handle bad user inputs so that the user receives useful messages if they enter invalid inputs (e.g. non integers, values outside the valid range). You might want to reuse some of your existing code for this if you have already implemented input validation.
  5. Write a function called get_max_cards(), which fetches the user’s saved "max_cards" setting from the user.json file. If the file does not exist or the user does not have a saved "max_cards" setting, then a default value is returned.

  6. Modify the code associated with the menu option to set the preferred number of cards per session so that when the user changes the number, this new number is saved to the JSON file. Remember that unlike for session history data, which is simply appended, when max_cards is changed, you can simply replace the existing value in the dictionary structure that is written out to user.json.

  7. Modify any code that accesses the max_cards variable so that it instead calls the get_max_cards() function.

Run and check: Run your code in the terminal to make sure it works with the command

python flashcards_app`
  • Select the menu option to change the value of max_cards, set a new value, then check your JSON file to see if the value has been saved correctly.
  • Close the app, open it again, and go through a session to check if the number of cards coincides with what you set.

Read through and add comments: Add any comments in your code that will help you understand it when you come back to it later.

Save your progress: Commit with message “EXERCISE 22: Add session-spanning user preference setting” and save your work to Github with the standard Git workflow.

(Re)read the guides:

Example solution

flashcards_app.py

# Created by: Alex Ubuntu
# Date: 01.01.2026
# Purpose: A personal flashcard trainer to help with learning

# Import the random module
import random
# Import json module
import json

### LOAD FLASHCARDS
# Set flashcards list from file
# Initialize empty list
flashcards_list = []
file_separator = ','

# Open and read the file
flashcard_file = "flashcards.txt"
try:
    with open(flashcard_file, 'r') as file:
        lines = file.readlines()
except:
    # Handle scenario where file is not found 
    # and display comprehensible message to user.
    print(f"The file {flashcard_file} is missing. Please add it to initialise your flashcards.")
    exit()

# Process each line
# Each line has: question,answer
for line in lines:
    # Remove whitespace/newlines
    line = line.strip()
    
    # Extract question and answer 
    # with pattern matching assuming two comma
    # separated values in a row:
    question, answer = line.split(file_separator)
    
    # Add to list
    flashcards_list.append((question, answer))

# Confirm loaded
print(f"{len(flashcards_list)} flashcards loaded!")

### LOAD USER DATA FILE/FIRST INTERACTION WITH USER
# JSON structure for user data:
#{
#    "max_cards": 10, 
#    "progress_history": [
#    {
#        "num_cards_practiced": 10,
#        "score": 0.8,
#    },
#    {
#        "num_cards_practiced": 10,
#        "score": 0.6,
#    }
#]
#}

# Filename for user details
user_filename = "user.json"

# Welcome message
print("Welcome to your personal flashcard trainer!")

# Initialise name variable to None since it will later be
# set either by reading from file or asking the user
name = ""
# Absolute maximum number of cards so that the user can't ask for too many
ABSOLUTE_MAX_CARDS = 100
DEFAULT_MAX_CARDS = 20
max_cards = DEFAULT_MAX_CARDS # Set a default value for max_cards
try: 
    # Try and set name and max_cards from values saved in file
    with open(user_filename, 'r') as file:
        user_data = json.load(file)
        name = user_data.get("name") # Value stored in dictionary
        if not name:
            # If name not found, ask user for it
            name = input("What is your name? ")
            user_data["name"] = name
        
        # If max_cards not found in dictionary, 
        # set to default value
        max_cards = user_data.get("max_cards", DEFAULT_MAX_CARDS)
        user_data["max_cards"] = max_cards
        # Save these values to file
        with open(user_filename, 'w') as file:
            json.dump(user_data, file, indent=4)
except FileNotFoundError:
    # If the file is not there, there is no existing user_data 
    # so we have to start with an empty dictionary and set the values
    # of "name" and "max_cards"
    user_data = {}
    name = input("What is your name? ")
    user_data["name"] = name
    # Since we already have default value for max_cards, don't ask
    # user (they can always change it later by choosing Option 1 below)
    user_data["max_cards"] = DEFAULT_MAX_CARDS
    with open(user_filename, 'w') as file:
        json.dump(user_data, file, indent=4)

# Confirm name
print(f"\nMy name is {name}")

# Card and score variables
num_cards_completed = 0
num_cards_correct = 0
score = 0

### FUNCTIONS
# Function to calculate and display score information
def display_score_info():

    # Handle case where no cards have been completed yet.
    if num_cards_completed <=0:
        print("You need to practice before you can get a score.")
    else:
        # Calculate score
        score = (num_cards_correct/num_cards_completed) * 100
        print(f"\nYou have answered {num_cards_correct} out of {num_cards_completed} correctly. Your score is {score}%.")

# Function to write score information to file
def write_score_info():
    # Calculate score
    score = (num_cards_correct/num_cards_completed) * 100
    data_to_append = {
        "num_cards_practiced": num_cards_completed,
        "score": score,
    }
    # Initialise user_data as empty dictionary
    user_data = {}
    # Initialise score_data as empty dictionary
    score_data = []
    # Try opening the json file
    # Use user_file variable from above
    try:
        with open(user_filename, 'r') as file:
            # Load file contents if found
            user_data = json.load(file)
            # Use safe extraction to get the progress_history 
            # containing the list of score data for different sessions. 
            # If it isn't there, set to empty list.
            score_data = user_data.get("progress_history", [])
    except FileNotFoundError:
        # Otherwise do nothing so score_data remains an empty list
        pass

    # Append latest session data
    score_data.append(data_to_append)

    # Write out score_data with the latest session data appended
    # to json file
    with open(user_filename, 'w') as file:
        # Save the score data in the progress_history element
        user_data["progress_history"] = score_data
        json.dump(user_data, file, indent=4)

# Fetches progress history from json file
# Empty list returned if history or file not found
def get_progress_history():
    score_data = []
    # Open and read the file
    try:
        with open(user_filename, 'r') as file:
            # Load file contents if found
            user_data = json.load(file)
            # Use safe extraction to get the progress_history 
            # containing the list of score data for different sessions. 
            # If it isn't there, set to empty list.
            return user_data.get("progress_history", [])
        
    except FileNotFoundError:
        return []
# Function to get max_cards from file
def get_max_cards():
    user_data = {}
    # Initialise user_data with saved file data if available
    try: 
        with open(user_filename, 'r') as file:
            user_data = json.load(file)
            max_cards = user_data.get("max_cards", DEFAULT_MAX_CARDS)
            return max_cards
    except FileNotFoundError:
        # If no value found in file, return the default value
        return DEFAULT_MAX_CARDS

# Function to set max_cards and save to json
def set_max_cards():
    user_data = {}
    # Initialise user_data with saved file data if available
    try: 
        with open(user_filename, 'r') as file:
            user_data = json.load(file)
            max_cards = user_data["max_cards"] # This is the current value
    except FileNotFoundError:
        pass

    while True:
        # Validation for input for maximum number of cards

        # Fetch input from user but don't attempt to convert the input string 
        # to int until certain it will work 
        entered_max_cards = input("\nHow many cards would you like to practice each session? ")

        # Check if input string represents an integer
        if entered_max_cards.isdigit():

            # Convert to integer data type
            entered_max_cards = int(entered_max_cards)
            
            # Check if value is within the value range 
            # (between 1 and the absolute maximum number of cards)
            if entered_max_cards > 0 and entered_max_cards < ABSOLUTE_MAX_CARDS:
                    
                # Set the max_cards variable with the user's preference
                max_cards = entered_max_cards
                # Confirm number maximum number of cards per session
                print(f"\nI want to practice at most {max_cards} cards per session")
                # Set the user_data dictionary "max_cards" field to the new value
                user_data["max_cards"] = max_cards 
                # Save dictionary with the new value to json
                with open(user_filename, 'w') as file:
                    json.dump(user_data, file, indent=4)
                break
            else:
                ## Let the user know what the range should be
                print(f"\nPlease enter a valid number bewteen 1 and {ABSOLUTE_MAX_CARDS}.")    
        else:
            print(f"\nPlease enter a whole number number over 0.") 

### APP MENU LOOP            
while True:
    print("\nSelect an option by entering a number")
    print("1: Set the number of cards you wish to practice")
    print("2: Start flashcards")
    print("3: Show the current score")
    print("4: View progress history") # Option to view progress history
    print("5: Exit")
    
    
    choice = input("\nChoose an option: ")
    
    if choice == "1":

        set_max_cards() 

    elif choice == "2":

        max_cards = get_max_cards()

        # Reset the number of cards completed for each session
        num_cards_completed = 0
        num_cards_correct = 0

        # Iterate through flashcards list of tuples
        # Each tuple element of the list contains a question, answer pair
        # Using a for loop means that the number of flashcards displayed
        # is limited by the number of items in flashcards so the user
        # may end up practicing fewer cards than they specified  
        for q, a in flashcards_list:
            # Ask user question and save response into variable
            user_answer = input(f"\nQuestion: {q}\n")
            
            # Increment the count of cards completed
            num_cards_completed += 1
            
            # Display user's answer and correct answer
            print(f"Your answer: {user_answer}, Correct answer: {a}")

            # Response deemed to be correct even if given in different case
            if user_answer.lower() == a.lower():
                
                # Increment count of cards answered correctly
                num_cards_correct += 1
                print("Correct")
            else:
                print("Incorrect")
        
            # Check if number of cards completed has hit the user's
            # preferred maximum number of cards
            if num_cards_completed >= max_cards:
                break
        
        print("\nWell done on completing your practice session!")
        # Write score info out with each session completed
        write_score_info()
        # Display score info at the end of a session
        display_score_info()

    elif choice == "3":

        display_score_info()

    elif choice == "4":
        
        score_data = get_progress_history()
        
        if not score_data:
            # Let user know if there is no progress history data yet
            print("We can't find any progress history for you yet.")
        else:
            print("How many sessions would you like to view?")
            num_sessions = int(input("Enter the number or 0 to show all: "))
            print("What order would you like to see them in?")
            print("1: Oldest")
            print("2: Most recent")
            order = input("Choose an option by entering 1 or 2: ")

            # If the user wants to see all sessions, set num_sessions
            # to the number of sessions available
            if num_sessions==0:
                num_sessions = len(score_data) 

            selected_sessions = score_data
            if order =="2":
                # Need to reverse order of list if 
                # most recent first is selected
                selected_sessions = selected_sessions[::-1]
                selected_sessions = selected_sessions[:num_sessions]
        
            #TODO: Add handling of invalid inputs

            # Extract num_cards_completed and score from each
            # session's dictionary structure
            for i, session in enumerate(selected_sessions):
                
                # Extract num_cards_completed and score
                # Name these variables something different 
                # (e.g. saved_cards_completed, saved_score) 
                # so they don't write over those being used by the rest of the program
                saved_cards_completed = session["num_cards_practiced"]
                saved_score = session["score"]

                # Include number (counting from 1 rather than 0) for readability 
                # But does not correspond to session number
                print(f"{i+1}: {saved_cards_completed} cards completed, score {saved_score}%")
                # TODO: Include date/time stamp?

    elif choice == "5":
        print(f"We hope you enjoyed your practice session today, {name}.")
        if num_cards_completed > 0:
            display_score_info()

            # Calculate score for feedback messages
            score = (num_cards_correct/num_cards_completed) * 100

            # Display feedback message based on score
            if score > 90 and score <= 100:
                print("Excellent work!")
            elif score > 70 and score <= 90:
                print("Good job!")
            elif score > 50 and score <= 70:
                print("Keep practicing!")
            elif score > 0 and score <= 50: 
                print("Need more study time!")
        
        print("Look forward to seeing you again soon!")
        break
    
    else:
        # Clarify instruction to get valid input
        print("\nInvalid value entered. Please make sure you enter just a single digit (no other words): 1, 2, 3, 4 or 5 to select an option.")

Take a break: 🍰