EXERCISE 22: Add session-spanning user preference setting
- Nested JSON
- Reading and writing JSON (revisited)
- Dictionaries (revisited)
- Menu system pattern (revisited)
- Fetching user input with
input()(revisited) - Basic
input()syntax (revisited) - Handling
input()return values (revisited) - Input validation pattern (revisited)
- Defining and calling functions (revisited)
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.
-
Modify your previous code to load and save the user’s progress history so that it takes into account this new structure.
-
Add code that attempts to initialise the values
nameandmax_cardsfromuser.jsonat the beginning of the program. -
If the
user.jsonfile is not found or the field"name"is not found in the dictionary structure, ask the user for their name (as you did previously). - Write a function called
set_max_cards(), which asks the user for their preferred number of cards per session, sets themax_cardsvariable, and then saves it touser.json. The function should:- Have a dictionary
user_datawhich can either be empty or initialised by reading from theuser.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 theuser_datadictionary and then writesuser_data(back) touser.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.
- Have a dictionary
-
Write a function called
get_max_cards(), which fetches the user’s saved"max_cards"setting from theuser.jsonfile. If the file does not exist or the user does not have a saved"max_cards"setting, then a default value is returned. -
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_cardsis changed, you can simply replace the existing value in the dictionary structure that is written out touser.json. - Modify any code that accesses the
max_cardsvariable so that it instead calls theget_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:
- Nested JSON
- Reading and writing JSON (revisited)
- Dictionaries (revisited)
- Fetching user input with
input()(revisited) - Basic
input()syntax (revisited) - Handling
input()return values (revisited) - Input validation pattern (revisited)
- Defining and calling functions (revisited)
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: 🍰
