The program I have been working on has been developed almost exclusively as "train-of-thought" style code. Things happen linearly (for the most part) thru the code. The goal of the program is to house Arabic language lessons, somewhat similar to how Duolingo works. Right now I have four questions hard coded in with pre-set "wrong" answers for each question. The questions always appear in the same order but the answers for each question are displayed in a random order. When the user is ready to select the answer they click on the image that represents the word they are being asked when then calls a function to check if the right answer has been selected. If the right answer has been selected then the buttons for the next questions are displayed but if a wrong answer is selected than the buttons for the next questions are hidden to force the user to find the right answer first.

The thing I would like help with the most, is moving my code into a class based structure. Eventually, I'd like my program to house 50 to 60 lessons and my current formatting would require thousands of lines of code - many of which would be just copies of this initial code. My understanding is that working with classes (in python) could help reduce the need for unnecessary code duplication but I am completely self taught and don't have a good understanding of how to move my existing program into a new format.

Down the line, I'd also like to make the order of questions asked to be randomized and to have randomized wrong answers (instead of the pre-set wrong answers I have currently) but I think this should come after I've migrated my code to a new coding style/format.

Of course I would appreciate any and all feedback as my goal is to not just make a language lesson app but to also learn good programming practices along the way.

Below is the bulk of my code which works - even if it is a bit clunky!

def Part1():

    def create_widgets_in_first_frame():        # Create the label for the frame

        global score
        score = 0               #Setting the initial score to zero.
        print("Your score is: ", score)

        global lives
        lives = 3               #Setting the initial number of lives.
        print("You have", lives, "lives")

        current_frame=first_frame               #Make the frame number generic so as to make copy/paste easier

        ##### These four lines of code below need to be updated for the MC questions. #####
        Image1 = ImagePath + "girl,bint-بنت.png"
        Image2 = ImagePath + "man,ragul-رجل.png"
        Image3 = ImagePath + "woman,sit-ست.png"
        word = "boy,walid-ولد.png"

        ##### These four lines of code properly break down the options into the right parts for the intended use. #####
        wordTransliteration = word.rsplit("-",1)[0].rsplit(",",1)[1]
        wordText = word.rsplit(".", 1)[0].rsplit("-", 1)[1]
        EwordText = word.rsplit(",",1)[0]
        wordImage = PhotoImage(file=(ImagePath + word))
        ##### Maine Question is written here. #####  
        L1 = Label(current_frame, text="What is '" + EwordText + "' in Arabic?",font=("Helvetica", 35))
        L1.grid(columnspan=4, row=0)
        ##### Radiobutton files are converted to images. #####
        answer1 = PhotoImage(file=Image1)
        answer2 = PhotoImage(file=Image2)
        answer3 = PhotoImage(file=Image3)
        correctanswer = wordImage

        global var
        var = IntVar()
        var.set(0)  #Sets the initial radiobutton selection to nothing

        def Transliteration():
            Transliteration = Label(current_frame, text=wordText + " is pronounced " + "'"+wordTransliteration+"'", font=("Helvetica", 35))
            Transliteration.grid(row=2, columnspan=4)
        ##### Makes the phonetic pronunciation button. #####
        transliteration_button = Button(current_frame, text="Show Transliteration", command=Transliteration)
        transliteration_button.grid(column=0, row=4)

        choice1 = Radiobutton(current_frame, image=answer1, variable = var, value=1, command= Check_Answer)
        choice1.image = answer1       # This prevents python garbage collection from deleting the image pre-maturely.
        choice2 = Radiobutton(current_frame, image=answer2, variable = var, value=2, command= Check_Answer)
        choice2.image = answer2       # This prevents python garbage collection from deleting the image pre-maturely.
        choice3 = Radiobutton(current_frame, image=answer3, variable = var, value=3, command= Check_Answer)
        choice3.image = answer3       # This prevents python garbage collection from deleting the image pre-maturely.
        choice4 = Radiobutton(current_frame, image=correctanswer, variable = var, value=4, command= Check_Answer)
        choice4.image = correctanswer # This prevents python garbage collection from deleting the image pre-maturely.
        choices = [choice1, choice2, choice3, choice4]

        random.shuffle(choices) #This line of code randomizes the order of the radiobuttons. 
        choices[0].grid(row=1, column=0)
        choices[1].grid(row=1, column=1)
        choices[2].grid(row=1, column=2)
        choices[3].grid(row=1, column=3)

        # Creats the quit button and displays it.
        quit_button = Button(current_frame, text = "Quit", command = quit_program)
        quit_button.grid(column=4, row=4)

    def create_widgets_in_second_frame():
        current_frame=second_frame               #Make the frame number generic so as to make copy/paste easier

        ##### These four lines of code below need to be updated for the MC questions. #####
        Image1 = ImagePath + "boy,walid-ولد.png"
        Image2 = ImagePath + "man,ragul-رجل.png"
        Image3 = ImagePath + "woman,sit-ست.png"
        word = "girl,bint-بنت.png"

        ##### These four lines of code properly break down the options into the right parts for the intended use. #####
        wordTransliteration = word.rsplit("-",1)[0].rsplit(",",1)[1]
        wordText = word.rsplit(".", 1)[0].rsplit("-", 1)[1]
        EwordText = word.rsplit(",",1)[0]
        wordImage = PhotoImage(file=(ImagePath + word))
        ##### Maine Question is written here. #####  
        L1 = Label(current_frame, text="What is '" + EwordText + "' in Arabic?",font=("Helvetica", 35))
        L1.grid(columnspan=4, row=0)
        ##### Radiobutton files are converted to images. #####
        answer1 = PhotoImage(file=Image1)
        answer2 = PhotoImage(file=Image2)
        answer3 = PhotoImage(file=Image3)
        correctanswer = wordImage

        global var
        var = IntVar()
        var.set(0)  #Sets the initial radiobutton selection to nothing

        def Transliteration():
            Transliteration = Label(current_frame, text=wordText + " is pronounced " + "'"+wordTransliteration+"'", font=("Helvetica", 35))
            Transliteration.grid(row=2, columnspan=4)
        ##### Makes the phonetic pronunciation button. #####
        transliteration_button = Button(current_frame, text="Show Transliteration", command=Transliteration)
        transliteration_button.grid(column=0, row=4)

        choice1 = Radiobutton(current_frame, image=answer1, variable = var, value=1, command= Check_Answer)
        choice1.image = answer1       # This prevents python garbage collection from deleting the image pre-maturely.
        choice2 = Radiobutton(current_frame, image=answer2, variable = var, value=2, command= Check_Answer)
        choice2.image = answer2       # This prevents python garbage collection from deleting the image pre-maturely.
        choice3 = Radiobutton(current_frame, image=answer3, variable = var, value=3, command= Check_Answer)
        choice3.image = answer3       # This prevents python garbage collection from deleting the image pre-maturely.
        choice4 = Radiobutton(current_frame, image=correctanswer, variable = var, value=4, command= Check_Answer)
        choice4.image = correctanswer # This prevents python garbage collection from deleting the image pre-maturely.
        choices = [choice1, choice2, choice3, choice4]

        random.shuffle(choices) #This line of code randomizes the order of the radiobuttons. 
        choices[0].grid(row=1, column=0)
        choices[1].grid(row=1, column=1)
        choices[2].grid(row=1, column=2)
        choices[3].grid(row=1, column=3)

        # Creats the quit button and displays it.
        quit_button = Button(current_frame, text = "Quit", command = quit_program)
        quit_button.grid(column=4, row=4)

    def create_widgets_in_third_frame():
        current_frame=third_frame               #Make the frame number generic so as to make copy/paste easier

        ##### These four lines of code below need to be updated for the MC questions. #####
        Image1 = ImagePath + "boy,walid-ولد.png"
        Image2 = ImagePath + "girl,bint-بنت.png"
        Image3 = ImagePath + "man,ragul-رجل.png"
        word = "woman,sit-ست.png"

        ##### These four lines of code properly break down the options into the right parts for the intended use. #####
        wordTransliteration = word.rsplit("-",1)[0].rsplit(",",1)[1]
        wordText = word.rsplit(".", 1)[0].rsplit("-", 1)[1]
        EwordText = word.rsplit(",",1)[0]
        wordImage = PhotoImage(file=(ImagePath + word))
        ##### Maine Question is written here. #####  
        L1 = Label(current_frame, text="What is '" + EwordText + "' in Arabic?",font=("Helvetica", 35))
        L1.grid(columnspan=4, row=0)
        ##### Radiobutton files are converted to images. #####
        answer1 = PhotoImage(file=Image1)
        answer2 = PhotoImage(file=Image2)
        answer3 = PhotoImage(file=Image3)
        correctanswer = wordImage

        global var
        var = IntVar()
        var.set(0)  #Sets the initial radiobutton selection to nothing

        def Transliteration():
            Transliteration = Label(current_frame, text=wordText + " is pronounced " + "'"+wordTransliteration+"'", font=("Helvetica", 35))
            Transliteration.grid(row=2, columnspan=4)
        ##### Makes the phonetic pronunciation button. #####
        transliteration_button = Button(current_frame, text="Show Transliteration", command=Transliteration)
        transliteration_button.grid(column=0, row=4)

        choice1 = Radiobutton(current_frame, image=answer1, variable = var, value=1, command= Check_Answer)
        choice1.image = answer1       # This prevents python garbage collection from deleting the image pre-maturely.
        choice2 = Radiobutton(current_frame, image=answer2, variable = var, value=2, command= Check_Answer)
        choice2.image = answer2       # This prevents python garbage collection from deleting the image pre-maturely.
        choice3 = Radiobutton(current_frame, image=answer3, variable = var, value=3, command= Check_Answer)
        choice3.image = answer3       # This prevents python garbage collection from deleting the image pre-maturely.
        choice4 = Radiobutton(current_frame, image=correctanswer, variable = var, value=4, command= Check_Answer)
        choice4.image = correctanswer # This prevents python garbage collection from deleting the image pre-maturely.
        choices = [choice1, choice2, choice3, choice4]

        random.shuffle(choices) #This line of code randomizes the order of the radiobuttons. 
        choices[0].grid(row=1, column=0)
        choices[1].grid(row=1, column=1)
        choices[2].grid(row=1, column=2)
        choices[3].grid(row=1, column=3)

        # Creats the quit button and displays it.
        quit_button = Button(current_frame, text = "Quit", command = quit_program)
        quit_button.grid(column=4, row=4)

    def create_widgets_in_forth_frame():
        current_frame=forth_frame               #Make the frame number generic so as to make copy/paste easier

        ##### These four lines of code below need to be updated for the MC questions. #####
        Image1 = ImagePath + "boy,walid-ولد.png"
        Image2 = ImagePath + "girl,bint-بنت.png"
        Image3 = ImagePath + "woman,sit-ست.png"
        word = "man,ragul-رجل.png"

        ##### These four lines of code properly break down the options into the right parts for the intended use. #####
        wordTransliteration = word.rsplit("-",1)[0].rsplit(",",1)[1]
        wordText = word.rsplit(".", 1)[0].rsplit("-", 1)[1]
        EwordText = word.rsplit(",",1)[0]
        wordImage = PhotoImage(file=(ImagePath + word))
        ##### Maine Question is written here. #####  
        L1 = Label(current_frame, text="What is '" + EwordText + "' in Arabic?",font=("Helvetica", 35))
        L1.grid(columnspan=4, row=0)
        ##### Radiobutton files are converted to images. #####
        answer1 = PhotoImage(file=Image1)
        answer2 = PhotoImage(file=Image2)
        answer3 = PhotoImage(file=Image3)
        correctanswer = wordImage

        global var
        var = IntVar()
        var.set(0)  #Sets the initial radiobutton selection to nothing

        def Transliteration():
            Transliteration = Label(current_frame, text=wordText + " is pronounced " + "'"+wordTransliteration+"'", font=("Helvetica", 35))
            Transliteration.grid(row=2, columnspan=4)
        ##### Makes the phonetic pronunciation button. #####
        transliteration_button = Button(current_frame, text="Show Transliteration", command=Transliteration)
        transliteration_button.grid(column=0, row=4)

        choice1 = Radiobutton(current_frame, image=answer1, variable = var, value=1, command= Check_Answer)
        choice1.image = answer1       # This prevents python garbage collection from deleting the image pre-maturely.
        choice2 = Radiobutton(current_frame, image=answer2, variable = var, value=2, command= Check_Answer)
        choice2.image = answer2       # This prevents python garbage collection from deleting the image pre-maturely.
        choice3 = Radiobutton(current_frame, image=answer3, variable = var, value=3, command= Check_Answer)
        choice3.image = answer3       # This prevents python garbage collection from deleting the image pre-maturely.
        choice4 = Radiobutton(current_frame, image=correctanswer, variable = var, value=4, command= Check_Answer)
        choice4.image = correctanswer # This prevents python garbage collection from deleting the image pre-maturely.
        choices = [choice1, choice2, choice3, choice4]

        random.shuffle(choices) #This line of code randomizes the order of the radiobuttons. 
        choices[0].grid(row=1, column=0)
        choices[1].grid(row=1, column=1)
        choices[2].grid(row=1, column=2)
        choices[3].grid(row=1, column=3)

        # Creats the quit button and displays it.
        quit_button = Button(current_frame, text = "Quit", command = quit_program)
        quit_button.grid(column=4, row=4)

    def Check_Answer():
        global lives
        global score

        if str(var.get()) !="4":
            Answer_frame.grid_forget()
            check_frame.grid(column=1, row=0, padx=20, pady=5, sticky=(tkinter.W, tkinter.E))
            lives -=1

            Incorrect = Label(check_frame, text ="That's incorrect!\n Lives: " +str(lives) + "\n Score: " + str(score), font=("Helvetica", 35))
            Incorrect.grid(row=0, rowspan=2, column=2, columnspan=2)

        if str(var.get()) == "4":
            score +=1

            check_frame.grid_forget()
            Answer_frame.grid(column=1, row=0, padx=20, pady=5, sticky=(tkinter.W, tkinter.E))

            Correct = Label(Answer_frame, text = "That's right!\n Lives: " +str(lives)+ "\n Score: " + str(score), font=("Helvetica", 35))
            Correct.grid(row=0, rowspan=2, column=2, columnspan=2)
            first_frame_button = Button(Answer_frame, text = "Question 1", command = call_first_frame_on_top)
            first_frame_button.grid(column=1, row=3)
            second_frame_button = Button(Answer_frame, text = "Question 2", command = call_second_frame_on_top)
            second_frame_button.grid(column=2, row=3)
            third_frame_button = Button(Answer_frame, text = "Question 3", command = call_third_frame_on_top)
            third_frame_button.grid(column=3, row=3)
            forth_frame_button = Button(Answer_frame, text = "Question 4", command = call_forth_frame_on_top)
            forth_frame_button.grid(column=4, row=3)

    def call_first_frame_on_top():
        second_frame.grid_forget()
        third_frame.grid_forget()
        forth_frame.grid_forget()
        check_frame.grid_forget()
        Answer_frame.grid_forget()

        create_widgets_in_first_frame()

        first_frame.grid(column=0, row=0, padx=20, pady=5, sticky=(tkinter.W, tkinter.N, tkinter.E))

    def call_second_frame_on_top():
        first_frame.grid_forget()
        check_frame.grid_forget()
        third_frame.grid_forget()
        forth_frame.grid_forget()
        create_widgets_in_second_frame()
        Answer_frame.grid_forget()

        print(lives)
        if lives <= 0:
            quit_program

        second_frame.grid(column=0, row=0, padx=20, pady=5, sticky=(tkinter.W, tkinter.N, tkinter.E))

    def call_third_frame_on_top():
        check_frame.grid_forget()
        first_frame.grid_forget()
        second_frame.grid_forget()
        forth_frame.grid_forget()
        create_widgets_in_third_frame()
        Answer_frame.grid_forget()

        print(lives)
        if lives <= 0:
            quit_program
        third_frame.grid(column=0, row=0, padx=20, pady=5, sticky=(tkinter.W, tkinter.N, tkinter.E))

    def call_forth_frame_on_top():
        check_frame.grid_forget()
        first_frame.grid_forget()
        second_frame.grid_forget()
        third_frame.grid_forget()
        create_widgets_in_forth_frame()
        Answer_frame.grid_forget()

        print(lives)
        if lives <= 0:
            quit_program
        forth_frame.grid(column=0, row=0, padx=20, pady=5, sticky=(tkinter.W, tkinter.N, tkinter.E))

    def quit_program():
        root_window.destroy()

    ###############################
    # Main program starts here :) #
    ###############################
    screen = pygame.display.set_mode((1,1))

    Lesson1_FilePath = Root_File_Name + "Lessons\\Lesson_1\\"
    ImagePath = Lesson1_FilePath + "Images\\"

    # Create the root GUI window.
    root_window = Tk()

    root_window.title("Lesson 1: Part 1")

    # Define window size
    window_width = 200
    window_heigth = 100

    # Create frames inside the root window to hold other GUI elements. All frames must be created in the main program, otherwise they are not accessible in functions. 
    first_frame=tkinter.Frame(root_window, width=window_width, height=window_heigth)
    first_frame['borderwidth'] = 2
    first_frame['relief'] = 'sunken'
    first_frame.grid(column=0, row=0, padx=20, pady=5, sticky=(tkinter.W, tkinter.N, tkinter.E))

    second_frame=tkinter.Frame(root_window, width=window_width, height=window_heigth)
    second_frame['borderwidth'] = 2
    second_frame['relief'] = 'sunken'
    second_frame.grid(column=0, row=0, padx=20, pady=5, sticky=(tkinter.W, tkinter.N, tkinter.E))

    third_frame=tkinter.Frame(root_window, width=window_width, height=window_heigth)
    third_frame['borderwidth'] = 2
    third_frame['relief'] = 'sunken'
    third_frame.grid(column=0, row=0, padx=20, pady=5, sticky=(tkinter.W, tkinter.N, tkinter.E))

    forth_frame=tkinter.Frame(root_window, width=window_width, height=window_heigth)
    forth_frame['borderwidth'] = 2
    forth_frame['relief'] = 'sunken'
    forth_frame.grid(column=0, row=0, padx=20, pady=5, sticky=(tkinter.W, tkinter.N, tkinter.E))

    check_frame=tkinter.Frame(root_window, width=window_width, height=window_heigth)
    check_frame['borderwidth'] = 2
    check_frame['relief'] = 'sunken'
    check_frame.grid(column=1, row=0, padx=20, pady=5, sticky=(tkinter.W, tkinter.E))
    check_frame.grid_forget()

    Answer_frame=tkinter.Frame(root_window, width=window_width, height=window_heigth)
    Answer_frame['borderwidth'] = 2
    Answer_frame['relief'] = 'sunken'
    Answer_frame.grid(column=1, row=0, padx=20, pady=5, sticky=(tkinter.W, tkinter.E))
    Answer_frame.grid_forget()

    # Create the firist frame
    call_first_frame_on_top()

    # Start tkinter event - loop
    root_window.mainloop()
New contributor
Jack Duane is a new contributor to this site. Take care in asking for clarification, commenting, and answering. Check out our Code of Conduct.
up vote 4 down vote accepted

This got way out of hand for the comment section where it started, so let's make this an answer.

You want classes. That's good, since it's going to solve a lot of your repetition problems. Your code has a lot of repetition problems. But classes shouldn't be your main priority at this point. Putting the data outside of the code is more important.

Why? Couple of reasons.

Eventually, you'll have to do it anyway. You can't put 50 questions/answers in the code itself, it becomes a mess. A royal mess. Don't even try.

So the least you'd have to do is put it in an external script to be imported. However, since the only thing it will be holding is data, why not put it in a JSON file? Python happens to be quite good at parsing JSON and it's fairly human readable. Which brings me to the next point.

What happens when you want to modify a question, add one or remove one? Your current numbering system can't handle it without turning into a lot of work. For one minor modification. You want this program to have features similar to Duolingo. That's good, since it gives you something to compare it against. Duolingo has multiple questions per subject, multiple subjects per category and multiple categories per language combination.

{
  "languageCombo": "EnglishToArabic",
  "categories": {
    "name": "greetings",
    "subjects": {
      "name": "Family",
      "questions": {
        "id": 0,
        "question": "somethingSomethingEnglish",
        "possibleAnswers": {
          "possibility1": "somethingSomethingArabic1",
          "possibility2": "somethingSomethingArabic2",
          "possibility3": "somethingSomethingArabic3",
          "possibility4": "somethingSomethingArabic4",
        },
        "correctAnswer": "possibility3"
      }
    },
    "name": "travel",
    "subjects": {
    },
    "name": "food",
    "subjects": {
    }
  }
}

Roughly like that. All of a sudden you have a maintainable set of questions. And the above is already in JSON.

Now you also know what your classes would look like. What makes sense and what doesn't. Should you put this code into classes before extracting the dataset, you're doing double work. After all, the classes will look different after the extraction than before.

Extract the questions and answers first, worry about classes later. You'll see the code cleaning-up already quite nicely in the progress.

  • 1
    thanks for the answer - it makes a lot of sense and gives me a good direction to take my code. JSON here I come! – Jack Duane Sep 14 at 14:22
  • @JackDuane You're welcome. Feel free to come back for a follow-up (new) question when you've improved your code. – Mast Sep 14 at 14:25
  • I definitely will! – Jack Duane Sep 14 at 14:28

Your Answer

Jack Duane is a new contributor. Be nice, and check out our Code of Conduct.
 

By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

Not the answer you're looking for? Browse other questions tagged or ask your own question.