EXERCISE 23: Make app multi-user
- Nested JSON (revisited)
- 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)
- Sentinel loop pattern (revisited)
if(elif, andelse) statements (revisited)
Task: Extend your app so different users can use it, each with their preferences and progress saved to file.
You will need to extend your JSON structure again to store progress history data and preferred number of flashcards per session for multiple users. This will mean creating yet another level of nesting, with the top level identifying the user.
You might like to name the new file users.json rather than user.json since it stores data for multiple users.
E.g.
users.json
{
"user1": {
"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,
}]
},
"user2": {
"name": "User 2",
"max_cards": 30,
"progress_history": [
{
"num_cards_practiced": 30,
"score": 0.8,
}]
},
"user3": {
"name": "User 3",
"max_cards": 16,
"progress_history": [
{
"num_cards_practiced": 16,
"score": 0.5,
},
{
"num_cards_practiced": 16,
"score": 1.0,
}]
}
}
For this exercise you will ask your user to create a unique identifier the first time they use the app. This will then be used as the identifier (or key) for their data (in the above example, the identifiers are "user1", "user2", "user3").
You need to address the following:
- Ask the user if it is their first time using the app.
- If it is the user’s first time, ask them to create an identifier,
username. - Read in the dictionary data structure from
users.json. - Check to see if an identifier identical with the one the user has entered exists in the dictionary keys.
- If the identifier already exists in the dictionary keys, the user should be prompted to create a different identifier. This might need to happen more than once until the user has come up with an identifier that is unique;
- If it is not the user’s first time, ask for the user’s
usernameso this can be used to fetch their data. If it isn’t found, then they should be prompted to try again until it is. You might also like to set a maximum number of tries, after which the user should simply create a new identifier (username). - If the identifier entered by a new user does not exist in the dictionary keys, the user can be added with a key identifying them and a dictionary with a the default
max_cardsvalue. The dictionary should then be saved back tousers.json. - Previously written code where you have loaded and saved values (e.g. progress history,
max_cards) to theuser_datadictionary structure now has to be modified so that the data for a specific user is saved to the master dictionary structure holding data for all users. Theusernameis always the key used to access the user’s data.
Run and check: Run your code in the terminal to make sure it works with the command
python flashcards_app`
- Start the app and tell it that you are a new user. Create an identifier, practice a few sessions of flashcards and view your progress history, then exit the app.
- Start the app and tell it that you are an existing user, enter your identifier and check you can still see your progress history, then exit the app.
- Start the app and tell it that you are a new user. Try entering the same identifier as you previously used. You should get a message telling you to create a different identifier. Give a different identifier and then exit the app without practising any flashcards.
- Start the app and tell it that you are an existing user, enter this second identifier and try to see your progress history. You should see nothing or a message saying that you haven’t done any practice yet.
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 23: Make app multi-user” with the standard Git workflow.
(Re)read the guides:
- Nested JSON (revisited)
- 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)
- Sentinel loop pattern (revisited)
if(elif, andelse) statements (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 for multiple users:
#{
# "user1": {
# "max_cards": 10,
# "name": "User 1",
# "progress_history": [
# {
# "num_cards_practiced": 10,
# "score": 0.8,
# },
# {
# "num_cards_practiced": 10,
# "score": 0.6,
# },
# {
# "num_cards_practiced": 10,
# "score": 0.5,
# }]
# },
# "user2": {
# "name": "User 2",
# "max_cards": 30,
# "progress_history": [
# {
# "num_cards_practiced": 30,
# "score": 0.8,
# }]
# },
# "user3": {
# "name": "User 3",
# "max_cards": 16,
# "progress_history": [
# {
# "num_cards_practiced": 16,
# "score": 0.5,
# },
# {
# "num_cards_practiced": 16,
# "score": 1.0,
# }]
# }
#}
# Filename for user details
user_filename = "users.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
# Variable to limit the number of times the user
# can enter their username incorrectly before they
# are forced to create a new one.
MAX_USERNAME_ATTEMPTS = 3
# Initialise username to None since we need to set it explicitly
# when the program starts
username = None
name = ""
# Absolute maximum number of cards so that the user can't ask for too many
ABSOLUTE_MAX_CARDS = 100
DEFAULT_MAX_CARDS = 20 # Set a default value for max_cards
### SET THE USER AND INITIALISE PREFERENCES
# Check if it is the user's first time
print("Is it your first time using this app?")
# Input validation - ask question until answered correctly
while True:
first_time = input("0: No \n1: Yes\n")
# 0: It is not the user's first time, so they should have a username
# 1: It is the user's first time, so they don't have a username yet
if first_time!="0" and first_time!="1":
# Any input other than 0 or 1 is not valid so we need to prompt the user again
# with a clarifying message
print(f"Please enter either 0, if you have never used the app before or 1, if you have.")
else:
# Irrespective of whether the user has used the app before,
# try to open the user_data file and load the data in it,
# otherwise initialise with empty dictionary
all_user_data = {}
try:
with open(user_filename, 'r') as file:
all_user_data = json.load(file)
# Attempt to find name and max_cards in file or ask from user
num_attempts = 0
if first_time=="0":
while num_attempts < MAX_USERNAME_ATTEMPTS:
username = input("Enter your username: ")
# If it's not the user's first time, we need to get their data
user_data = all_user_data.get(username)
if isinstance(user_data, dict):
# If we find the data corresponding to the username, we can set the name
# and max_cards variables from the saved values or by asking the user
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, set to default value - the existing
# value of max_cards
max_cards = user_data.get("max_cards", DEFAULT_MAX_CARDS) # Value stored in dictionary
# Save the user_data back to the master dictionary all_user_data
all_user_data[username] = user_data
# Save these values to file
with open(user_filename, 'w') as file:
json.dump(all_user_data, file, indent=4)
# Leave the loop
break
else:
print("We can't find you in our database, are you sure you've spelt it correctly?\n")
num_attempts += 1
## If either it is the user's first time or the maximum number of attempts has been
# reached, the user needs to create a new username
if first_time=="1" or num_attempts >= MAX_USERNAME_ATTEMPTS:
while True:
# Keep asking the user for a username until they
# choose one that doesn't exist in all_user_data
username = input("Create a username: ")
# Check if the username is one of the keys in all_user_data
if username in all_user_data:
# If it already exists, prompt the user to create a different one
print("This username already exists, please create a different one")
else:
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
# Save this user_data to the master dictionary all_user_data with
# the username as the key
all_user_data[username] = user_data
with open(user_filename, 'w') as file:
json.dump(all_user_data, file, indent=4)
# Break out of loop
break
# Break out of outer loop
break
except FileNotFoundError:
# Since the file is not there, there is no existing user_data
# so we have to start with empty dictionary and ask for the username
user_data = {}
username = input("Create a username: ")
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
# Save this user_data to the master dictionary all_user_data with
# the username as the key
all_user_data[username] = user_data
with open(user_filename, 'w') as file:
json.dump(all_user_data, file, indent=4)
# Break out of loop
break
# Confirm name
print(f"\nYour 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,
}
# TODO: Include date/time stamp?
# Initialise all_user_data as empty dictionary
all_user_data = {}
# 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
all_user_data = json.load(file)
user_data = all_user_data[username]
# 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
# Save the user_data to the user's data in user_data
all_user_data[username] = user_data
json.dump(all_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
all_user_data = json.load(file)
user_data = all_user_data[username]
# 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():
all_user_data = {}
user_data = {}
# Initialise user_data with saved file data if available
try:
with open(user_filename, 'r') as file:
all_user_data = json.load(file)
user_data = all_user_data[username]
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 for user to set and save a max_cards value to json
def set_max_cards():
all_user_data = {}
user_data = {}
# Initialise user_data with saved file data if available
try:
with open(user_filename, 'r') as file:
all_user_data = json.load(file)
user_data = all_user_data[username]
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:
# Confirm number maximum number of cards per session
print(f"\nYou want to practice at most {entered_max_cards} cards per session")
# Set the user_data dictionary "max_cards" field to the user entered value
user_data["max_cards"] = entered_max_cards
# Update all_user_data dictionary to include this new setting
all_user_data[username] = user_data
# Save all_user_data dictionary with the new value to json
with open(user_filename, 'w') as file:
json.dump(all_user_data, file, indent=4)
break
else:
## Let the user know what the range should be
print(f"\nPlease enter a valid number between 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]
# Select only the rows up to the number of sessions
# the user wishes to be included
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: 🛋️
