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
Asked By: 324

||

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
Answered By: ErnestBidouille

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.

Answered By: hc_dev