Python Questionnaire Validation with Recursion
Question:
I’ve created a function in Python to review a series of questions in a dictionary and remove records from the dataset depending on the answers to the questionnaire. I’ve created the questionnaire so the user can only respond yes or no. I have a third branch to handle the case where the user responds with something other than yes or no, but I want the function to repeat the existing question. Currently, it goes back and restarts from the first question. What can I add to fix this functionality?
For reference, here is the function I created:
def questionnaire(df, questions = questions):
for i in range(0, len(questions)): #len(questions)-1):
print(list(questions.keys())[i])
ans = str(input())
if ans.lower() == 'yes':
if list(questions.values())[i][1] == "Yes":
df = df[df.Section.isin(list(questions.values())[i][0]) == False]
elif ans.lower() == 'no':
if list(questions.values())[i][1] == "No":
df = df[df.Section.isin(list(questions.values())[i][0]) == False]
else:
print("Please type yes or no.")
questionnaire(df, questions = questions)
return df
Answers:
This should work for your problem:
def questionnaire(df, questions=questions):
for i in range(0, len(questions)): #len(questions)-1):
print(list(questions.keys())[i])
ans = None
while ans not in ['yes', 'no']:
print("Please type yes or no.")
ans = str(input()).lower()
if ans == 'yes':
if list(questions.values())[i][1] == "Yes":
df = df[df.Section.isin(list(questions.values())[i][0]) ==
False]
else:
if list(questions.values())[i][1] == "No":
df = df[df.Section.isin(list(questions.values())[i][0]) ==
False]
return df
So you can transform it to this code:
def questionnaire(df, questions=questions):
for i in range(0, len(questions)): #len(questions)-1):
print(list(questions.keys())[i])
ans = None
while ans not in ['yes', 'no']:
print("Please type yes or no.")
ans = str(input()).lower()
question = list(questions.values())[i]
if question[1] == ans.title():
df = df[df.Section.isin(question[0]) == False]
return df
Issue: Recursion, restarting the entire questionnaire again
You have so many lines in that loop over questions that you might not see the recursion:
def questionnaire(df, questions = questions):
for i in range(0, len(questions)): #len(questions)-1):
# your code omitted for clarity
else:
print("Please type yes or no.")
questionnaire(df, questions = questions) # RECURSION! Starting the questionnaire over again
Let’s clean the scene with some refactoring first. Then we can try to fix the issue. Instead of recursion we need a loop.
Refactor: Extract functions to have the loop short and clean
First let us add some comments that explain the abstractions (what your code is intended to do):
def questionnaire(df, questions = questions):
# ask all questions, one at a time
for i in range(0, len(questions)): # for each question
print(list(questions.keys())[i]) # ask current question
ans = str(input()) # get user input as answer
if ans.lower() == 'yes':
# verify if given answer (title-cased YES) was expected
if list(questions.values())[i][1] == "Yes":
df = df[df.Section.isin(list(questions.values())[i][0]) == False]
elif ans.lower() == 'no':
# verify if given answer (title-cased NO) was expected
if list(questions.values())[i][1] == "No":
df = df[df.Section.isin(list(questions.values())[i][0]) == False]
else:
# if given answer is invalid (not in set of expected answers), ask again
print("Please type yes or no.")
questionnaire(df, questions = questions)
return df
Using the comments we can see similarities and refactor this.
Also taking some improvements:
- assume
questions
is a Python dict of key question
mapped to value answer_options
.
Instead of iterating with for-i and then indexing you can use for-each like for question, answer_tuple in questions_dict.items()
then replace list(questions.keys())[i]
to question
and list(questions.values())[i]
to answer_tuple
.
- instead
print(question); ans = input()
you can directly prompt input(question)
def ask(question):
# ask current question
return str(input(question)) # get user input as answer
def store_answer(df, expected)
return df[df.Section.isin(expected) == False]
def collect_answer(df, question, answer_tuple):
ans = ask(question)
while ans.lower() not in {'yes', 'no'}:
print("Invalid answer. Please type either yes or no.")
ans = ask(question)
# valid answer to store
if ans.title() == answer_tuple[1]: # if correct answer given
return store_answer(df, answer_tuple[0])
return df
def questionnaire(df, questions_dict = questions):
# ask all questions, one at a time
for question, answer_tuple in questions_dict.items(): # for each dict entry (question with answer_tuple)
df = collect_answer(df, question, answer_tuple)
return df
Fixed by Pattern: Loop until valid input
Instead of the recursion with questionnaire(df, questions)
we just ask for the current question again in a loop until valid input was given:
ans = ask(question)
while ans.lower() not in {'yes', 'no'}:
print("Invalid answer. Please type either yes or no.")
ans = ask(question)
# valid answer to store
If no valid answer was given, then it only asks the current question again … as long as the answer is not in the set of valid options {'yes', 'no'}
.
As soon as this while
loop exits, we know the answer is valid and can continue.
I’ve created a function in Python to review a series of questions in a dictionary and remove records from the dataset depending on the answers to the questionnaire. I’ve created the questionnaire so the user can only respond yes or no. I have a third branch to handle the case where the user responds with something other than yes or no, but I want the function to repeat the existing question. Currently, it goes back and restarts from the first question. What can I add to fix this functionality?
For reference, here is the function I created:
def questionnaire(df, questions = questions):
for i in range(0, len(questions)): #len(questions)-1):
print(list(questions.keys())[i])
ans = str(input())
if ans.lower() == 'yes':
if list(questions.values())[i][1] == "Yes":
df = df[df.Section.isin(list(questions.values())[i][0]) == False]
elif ans.lower() == 'no':
if list(questions.values())[i][1] == "No":
df = df[df.Section.isin(list(questions.values())[i][0]) == False]
else:
print("Please type yes or no.")
questionnaire(df, questions = questions)
return df
This should work for your problem:
def questionnaire(df, questions=questions):
for i in range(0, len(questions)): #len(questions)-1):
print(list(questions.keys())[i])
ans = None
while ans not in ['yes', 'no']:
print("Please type yes or no.")
ans = str(input()).lower()
if ans == 'yes':
if list(questions.values())[i][1] == "Yes":
df = df[df.Section.isin(list(questions.values())[i][0]) ==
False]
else:
if list(questions.values())[i][1] == "No":
df = df[df.Section.isin(list(questions.values())[i][0]) ==
False]
return df
So you can transform it to this code:
def questionnaire(df, questions=questions):
for i in range(0, len(questions)): #len(questions)-1):
print(list(questions.keys())[i])
ans = None
while ans not in ['yes', 'no']:
print("Please type yes or no.")
ans = str(input()).lower()
question = list(questions.values())[i]
if question[1] == ans.title():
df = df[df.Section.isin(question[0]) == False]
return df
Issue: Recursion, restarting the entire questionnaire again
You have so many lines in that loop over questions that you might not see the recursion:
def questionnaire(df, questions = questions):
for i in range(0, len(questions)): #len(questions)-1):
# your code omitted for clarity
else:
print("Please type yes or no.")
questionnaire(df, questions = questions) # RECURSION! Starting the questionnaire over again
Let’s clean the scene with some refactoring first. Then we can try to fix the issue. Instead of recursion we need a loop.
Refactor: Extract functions to have the loop short and clean
First let us add some comments that explain the abstractions (what your code is intended to do):
def questionnaire(df, questions = questions):
# ask all questions, one at a time
for i in range(0, len(questions)): # for each question
print(list(questions.keys())[i]) # ask current question
ans = str(input()) # get user input as answer
if ans.lower() == 'yes':
# verify if given answer (title-cased YES) was expected
if list(questions.values())[i][1] == "Yes":
df = df[df.Section.isin(list(questions.values())[i][0]) == False]
elif ans.lower() == 'no':
# verify if given answer (title-cased NO) was expected
if list(questions.values())[i][1] == "No":
df = df[df.Section.isin(list(questions.values())[i][0]) == False]
else:
# if given answer is invalid (not in set of expected answers), ask again
print("Please type yes or no.")
questionnaire(df, questions = questions)
return df
Using the comments we can see similarities and refactor this.
Also taking some improvements:
- assume
questions
is a Python dict of keyquestion
mapped to valueanswer_options
.
Instead of iterating with for-i and then indexing you can use for-each likefor question, answer_tuple in questions_dict.items()
then replacelist(questions.keys())[i]
toquestion
andlist(questions.values())[i]
toanswer_tuple
. - instead
print(question); ans = input()
you can directly promptinput(question)
def ask(question):
# ask current question
return str(input(question)) # get user input as answer
def store_answer(df, expected)
return df[df.Section.isin(expected) == False]
def collect_answer(df, question, answer_tuple):
ans = ask(question)
while ans.lower() not in {'yes', 'no'}:
print("Invalid answer. Please type either yes or no.")
ans = ask(question)
# valid answer to store
if ans.title() == answer_tuple[1]: # if correct answer given
return store_answer(df, answer_tuple[0])
return df
def questionnaire(df, questions_dict = questions):
# ask all questions, one at a time
for question, answer_tuple in questions_dict.items(): # for each dict entry (question with answer_tuple)
df = collect_answer(df, question, answer_tuple)
return df
Fixed by Pattern: Loop until valid input
Instead of the recursion with questionnaire(df, questions)
we just ask for the current question again in a loop until valid input was given:
ans = ask(question)
while ans.lower() not in {'yes', 'no'}:
print("Invalid answer. Please type either yes or no.")
ans = ask(question)
# valid answer to store
If no valid answer was given, then it only asks the current question again … as long as the answer is not in the set of valid options {'yes', 'no'}
.
As soon as this while
loop exits, we know the answer is valid and can continue.