3
\$\begingroup\$

In part one (Tkinter program to teach Arabic) I was asking for help to review the part of the program responsible for managing the main screens and links to all of the different lessons. From the responses I got, I was able to reduce a lot of redundancies in my code thru the use of classes (a new skill for me) and was able to clean my code up to comply with PEP 8 and hopefully improve the readability of the code.

In this post, I am looking at the second part of my program. This part is stored in a separate .py file and is responsible for loading the actual content of a lesson selected from the main program.

I'm hoping to get some pointers on the use of global variables. Almost everything I can read on the internet talks about how bad they are but for me I can't quite see how I could do what I'm doing without them. I have five different variables declared as global. Any commentary on why they might be good or bad for my program would be very helpful. If they do need to go, then suggestions on "how to" would be helpful.

"""
Module Docstring:
This is the lesson template for almost all of the lesson plans Liblib Aribi.
It includes different types of quesitons such as: multiple choice, entry box, matching, and sentance re-ordering questions.
"""
import os
import random
import winsound
import tkinter
##from tkinter import *
from tkinter import Label, Button, IntVar, Radiobutton, PhotoImage, Entry, Toplevel, ttk
import json
SCORE = None #Defining Global Variables
LIVES = None
VAR = None
WORD_POSITION = None
USER_ANSWER = None
STANDARD_CONFIGS = {
 "active":
 {"rel": tkinter.SUNKEN, "color": "gold"},
 "inactive":
 {"rel": tkinter.RAISED, "color": "gray95"},
 "width": "100",
 "height": "100",
 "bg": "gray",
 "font": {
 "bodytext": ("Helvetica", 15),
 "title": ("Helvetica", 35),
 "title2": ("Helvetica", 25),
 "secondaryhedding": ("Helvetica", 20)
 },
 "borderwidth": "2",
 "disabled": tkinter.DISABLED
 }
class CheckAnswers:
 """
 This class checkes the given answer against the stated correct answer
 and adjusts the scoring and live counter accordinly.
 """
 def __init__(self, sf, cho, noa):
 self.specialframes = sf
 self.choices = cho
 self.no_of_answers = noa
 def wrong_answer(self):
 global LIVES
 global SCORE
 self.specialframes[1].grid_remove() #Right answer frame
 self.specialframes[0].grid(column=1, row=0, rowspan=2) #Wrong answer frame
 LIVES -= 1
 incorrect_button = Label(
 self.specialframes[0],
 text=f"That's incorrect!\n Lives: {str(LIVES)}\n Score: {str(SCORE)}",
 font=STANDARD_CONFIGS["font"]["title"])
 incorrect_button.grid(row=0, rowspan=2, column=0, columnspan=3)
 def right_answer(self):
 global LIVES
 global SCORE
 self.specialframes[0].grid_remove() #Wrong answer frame
 self.specialframes[1].grid(column=1, row=0, rowspan=2) #Right answer frame
 correct_button = Label(
 self.specialframes[1],
 text=f" That's right! \n Lives: {str(LIVES)}\n Score: {str(SCORE)}",
 font=STANDARD_CONFIGS["font"]["title"])
 correct_button.grid(row=0, rowspan=2, column=0, columnspan=5)
 if self.no_of_answers is not None:
 for i in range(self.no_of_answers):
 self.choices[i].config(state=STANDARD_CONFIGS["disabled"])
def lesson_template(lesson_name, jfn, lesson_type):
 """
 This function loads a pre-determined json file to quiz the user.
 """
 root_file_name = "C:\\LearningArabic\\LiblibArriby\\"
 lesson_file_path = f"{root_file_name}Lessons\\{lesson_name}\\"
 with open(
 f"{lesson_file_path}{jfn}.json",
 "r",
 encoding="utf-8-sig") as question_file:
 data = json.load(question_file)
 no_of_questions = (len(data["lesson"])) #Counts the number of questions in the .json file
 if lesson_type == "quiz":
 no_of_quiz_questions = 8
 if lesson_type == "short":
 no_of_quiz_questions = 4
 if lesson_type == "conversation":
 no_of_quiz_questions = 1
 if lesson_type == "long":
 no_of_quiz_questions = 8
 global WORD_POSITION
 WORD_POSITION = 0
 def question_frame_populator(frame_number, data, current_frame, question_number):
 question_directory = data["lesson"][question_number]
 question = question_directory.get("question")
 questiontype = question_directory.get("questiontype")
 correctanswer = question_directory.get("answer")
 arabic = question_directory.get("arabic")
 english_transliteration = question_directory.get("transliteration")
 if english_transliteration is None:
 english_transliteration = "N/A"
 sound = question_directory.get("pronounciation")
 image = question_directory.get("image")
 image_location = os.path.join(image_path, f"{image}")
 if questiontype == "mc":
 if os.path.isfile(image_location):
 arabic_image = PhotoImage(file=image_location)
 image_label = Label(current_frame, image=arabic_image)
 image_label.image = arabic_image
 image_label.grid(row=1, rowspan=4, column=0, columnspan=2)
 maine_question = Label(
 current_frame,
 text=question,
 font=STANDARD_CONFIGS["font"]["title"],
 wraplength=500)
 transliteration_button = Button(
 current_frame,
 text="Show Transliteration",
 font=STANDARD_CONFIGS["font"]["bodytext"],
 command=lambda: transliteration(
 current_frame,
 arabic,
 english_transliteration))
 next_button(frame_number) # Creates the "next" button and displays it.
 quit_program_button(current_frame) # Creates the "quit" button and displays it.
 pronounciation_button = Button(
 current_frame,
 text="Listen",
 font=STANDARD_CONFIGS["font"]["bodytext"],
 command=lambda: pronounciation(sound))
 maine_question.grid(columnspan=4, row=0)
 if english_transliteration != "N/A":
 transliteration_button.grid(column=0, row=5, columnspan=2)
 pronounciation_button.grid(column=2, row=5, columnspan=1)
 wronganswer = question_directory["wronganswer"]
 global VAR
 VAR = IntVar()
 VAR.set(0) #Sets the initial radiobutton selection to nothing
 answers = generate_answers(wronganswer, correctanswer)
 no_of_answers = len(answers)
 choices = []
 for i in range(4):
 choice = Radiobutton(
 current_frame,
 text=answers[i],
 variable=VAR,
 value=i+1,
 font=STANDARD_CONFIGS["font"]["bodytext"],
 command=lambda: check_mc_answer(choices, no_of_answers)
 )
 #choice.image = answers[i]
 choices.append(choice)
 random.shuffle(choices) #Randomizes the order of the radiobuttons.
 if os.path.isfile(image_location):
 choices[0].grid(row=1, column=2, columnspan=2)
 choices[1].grid(row=2, column=2, columnspan=2)
 choices[2].grid(row=3, column=2, columnspan=2)
 choices[3].grid(row=4, column=2, columnspan=2)
 if not os.path.isfile(image_location):
 choices[0].grid(row=1, column=1, columnspan=2)
 choices[1].grid(row=2, column=1, columnspan=2)
 choices[2].grid(row=3, column=1, columnspan=2)
 choices[3].grid(row=3, column=1, columnspan=2)
 if questiontype == "eb":
 if os.path.isfile(image_location):
 arabic_image = PhotoImage(file=image_location)
 image_label = Label(current_frame, image=arabic_image)
 image_label.image = arabic_image
 image_label.grid(row=1, rowspan=3, column=0, columnspan=2)
 main_question = Label(
 current_frame,
 text=question,
 font=STANDARD_CONFIGS["font"]["title"],
 wraplength=500)
 transliteration_button = Button(
 current_frame,
 text="Show Transliteration",
 font=STANDARD_CONFIGS["font"]["bodytext"],
 command=lambda: transliteration(
 current_frame,
 arabic,
 english_transliteration))
 next_button(frame_number) # Creates the "next" button and displays it
 quit_program_button(current_frame) # Creates the "quit" button
 pronounciation_button = Button(
 current_frame,
 text="Listen",
 font=STANDARD_CONFIGS["font"]["bodytext"],
 command=lambda: pronounciation(sound))
 entry_box = Entry(
 current_frame,
 width=20,
 font=STANDARD_CONFIGS["font"]["secondaryhedding"])
 entry_box.focus()
 check_answer_button = Button(
 current_frame,
 text="Check Answer",
 font=STANDARD_CONFIGS["font"]["secondaryhedding"],
 command=lambda: check_entry_box(entry_box, correctanswer))
 main_question.grid(column=0, row=0, columnspan=4)
 entry_box.grid(column=1, row=1, columnspan=2)
 check_answer_button.grid(column=1, row=3, columnspan=2)
 if english_transliteration != "N/A":
 transliteration_button.grid(column=0, row=5, columnspan=2)
 pronounciation_button.grid(column=2, row=5, columnspan=1)
 if questiontype == "matching":
 no_of_statements = (len(data["lesson"]))
 list_of_sentances = []
 list_of_answers = []
 list_of_sounds = []
 for i in range(no_of_statements):
 directory = data["lesson"][f"question{str(i)}"]
 sentance = directory.get("question")
 list_of_sentances.append(sentance)
 answer = directory.get("answer")
 list_of_answers.append(answer)
 sound = directory.get("pronounciation")
 list_of_sounds.append(sound)
 paired = []
 for i in range(no_of_statements):
 pair = (list_of_sentances[i],
 list_of_answers[i],
 list_of_sounds[i])
 paired.append(pair)
 random.shuffle(paired)
 for i in range(no_of_statements):
 sound_button = Button(
 current_frame,
 text="Listen",
 font=STANDARD_CONFIGS["font"]["bodytext"],
 command=lambda i=i: pronounciation(paired[i][2]))
 sound_button.grid(column=0, row=i+1)
 for i in range(no_of_statements):
 label_1 = Label(
 current_frame,
 text=paired[i][0],
 font=STANDARD_CONFIGS["font"]["bodytext"])
 label_1.grid(column=1, row=1+i)
 list_of_entry_boxes = []
 for i in range(no_of_statements):
 entry_box_1 = Entry(
 current_frame,
 width=10,
 font=STANDARD_CONFIGS["font"]["bodytext"])
 entry_box_1.grid(column=2, columnspan=1, row=i+1)
 list_of_entry_boxes.append(entry_box_1)
 main_quesion = Label(
 current_frame,
 text="Use the entry boxes on the right to correctly order the conversation",
 font=STANDARD_CONFIGS["font"]["secondaryhedding"],
 wraplength=500)
 main_quesion.grid(columnspan=4, row=0)
 check_answer_button = Button(
 current_frame,
 text="Check Answer",
 font=STANDARD_CONFIGS["font"]["bodytext"],
 command=lambda: check_multiple_entry_boxes(
 list_of_entry_boxes,
 paired,
 current_frame,
 no_of_statements))
 check_answer_button.grid(column=0, columnspan=4, row=8)
 if questiontype == "wordbank":
 current_frame.grid_rowconfigure(1, weight=1)
 current_frame.grid_rowconfigure(2, weight=1)
 if os.path.isfile(image_location):
 arabic_image = PhotoImage(file=image_location)
 image_label = Label(current_frame, image=arabic_image)
 image_label.image = arabic_image
 image_label.grid(row=1, rowspan=4, column=0, columnspan=2)
 question_title = Label(
 current_frame,
 text=question,
 font=STANDARD_CONFIGS["font"]["title"],
 wraplength=500)
 transliteration_button = Button(
 current_frame,
 text="Show Transliteration",
 font=STANDARD_CONFIGS["font"]["bodytext"],
 command=lambda: transliteration(
 current_frame,
 arabic,
 english_transliteration))
 check_answer_button = Button(
 current_frame,
 text="Check Answer",
 font=STANDARD_CONFIGS["font"]["bodytext"],
 command=lambda: check_sentance_order(solution, USER_ANSWER))
 clear_selection_button = Button(
 current_frame,
 text="Clear Selection",
 font=STANDARD_CONFIGS["font"]["bodytext"],
 command=lambda: clear_selected_words(
 word_bank,
 size_of_word_bank,
 word_chosen))
 pronounciation_button = Button(
 current_frame,
 text="Listen",
 font=STANDARD_CONFIGS["font"]["bodytext"],
 command=lambda: pronounciation(sound))
 sentance_label = Label(
 current_frame,
 text="Sentence:",
 font=STANDARD_CONFIGS["font"]["bodytext"])
 word_bank_label = Label(
 current_frame,
 text="Word Bank:",
 font=STANDARD_CONFIGS["font"]["bodytext"])
 size_of_solution = len(question_directory["solution"])
 size_of_word_bank = len(question_directory["answers"])
 sentance_components = []
 solution = []
 for i in range(size_of_solution):
 word = question_directory["solution"][i]
 solution.append(word)
 word_chosen = []
 for i in range(size_of_word_bank):
 word = question_directory["answers"][i]
 sentance_components.append(word)
 label = Label(current_frame,
 text=sentance_components[i],
 font=STANDARD_CONFIGS["font"]["bodytext"])
 word_chosen.append(label)
 global USER_ANSWER
 USER_ANSWER = []
 word_bank = []
 for i in range(size_of_word_bank):
 word = Button(
 current_frame,
 text=sentance_components[i],
 font=STANDARD_CONFIGS["font"]["bodytext"],
 command=lambda i=i: sentancebuilder(
 question_directory,
 word_bank,
 i,
 sentance_components,
 size_of_word_bank,
 word_chosen))
 word_bank.append(word)
 random.shuffle(word_bank)
 next_button(frame_number) # Creates the "next" button and displays it.
 question_title.grid(column=0, row=0, columnspan=6)
 sentance_label.grid(column=0, row=1, columnspan=1)
 word_bank_label.grid(column=0, row=2, columnspan=1)
 for i in range(size_of_word_bank):
 word_bank[i].grid(column=i+1, row=2, columnspan=1)
 pronounciation_button.grid(column=0, row=3, columnspan=1)
 clear_selection_button.grid(column=1, row=3, columnspan=2)
 check_answer_button.grid(column=3, row=3, columnspan=2)
 transliteration_button.grid(column=0, row=5, columnspan=2)
 def sentancebuilder(question_directory, word_bank, i, sentance_components, size_of_word_bank, word_chosen):
 for index in range(size_of_word_bank):
 if word_bank[index]["text"] == sentance_components[i]:
 word_bank[index].grid_forget()
 global WORD_POSITION
 if question_directory["translationdirection"] == "EtoA":
 word_chosen[i].grid(column=size_of_word_bank-WORD_POSITION, row=1, columnspan=1)
 if question_directory["translationdirection"] == "AtoE":
 word_chosen[i].grid(column=WORD_POSITION+1, row=1, columnspan=1)
 WORD_POSITION += 1
 global USER_ANSWER
 USER_ANSWER.append(sentance_components[i])
 def clear_selected_words(word_bank, size_of_word_bank, word_chosen):
 for i in range(size_of_word_bank):
 word_chosen[i].grid_forget()
 global WORD_POSITION
 WORD_POSITION = 0
 global USER_ANSWER
 USER_ANSWER = []
 for i in range(size_of_word_bank):
 word_bank[i].grid(column=i+1, row=2)
 def check_multiple_entry_boxes(list_of_entry_boxes, paired, current_frame, no_of_statements):
 entered_values = []
 for i in range(no_of_statements):
 value = list_of_entry_boxes[i].get()
 entered_values.append(value)
 for i in range(no_of_statements):
 if entered_values[i] == paired[i][1]:
 label_1 = Label(
 current_frame,
 text="Correct ",
 font=STANDARD_CONFIGS["font"]["bodytext"])
 label_1.grid(column=3, row=i+1)
 for i in range(no_of_statements):
 if entered_values[i] != paired[i][1]:
 label_1 = Label(
 current_frame,
 text="Incorrect",
 font=STANDARD_CONFIGS["font"]["bodytext"])
 label_1.grid(column=3, row=i+1)
 def check_sentance_order(solution, USER_ANSWER):
 if solution == USER_ANSWER:
 CheckAnswers(special_frames, None, None).right_answer()
 if solution != USER_ANSWER:
 CheckAnswers(special_frames, None, None).wrong_answer()
 def check_entry_box(entry_box, correctanswer):
 entered_value = entry_box.get()
 if entered_value == correctanswer:
 CheckAnswers(special_frames, None, None).right_answer()
 if entered_value != correctanswer:
 CheckAnswers(special_frames, None, None).wrong_answer()
 def check_mc_answer(choices, no_of_answers):
 if str(VAR.get()) != "4":
 CheckAnswers(special_frames, choices, no_of_answers).wrong_answer()
 if str(VAR.get()) == "4":
 CheckAnswers(special_frames, choices, no_of_answers).right_answer()
 def transliteration(current_frame, arabic, english_transliteration):
 transliteration_button = Label(
 current_frame,
 text=f"'{arabic}' is pronounced '{english_transliteration}'",
 font=STANDARD_CONFIGS["font"]["secondaryhedding"])
 transliteration_button.grid(row=5, column=0, columnspan=6)
 def pronounciation(sound):
 winsound.PlaySound(f"{sound_path}{sound}", winsound.SND_FILENAME)
 def generate_answers(wronganswer, correctanswer):
 wans = random.sample(wronganswer, 3)
 answers = wans
 answers.append(correctanswer)
 return answers
 def check_remaining_lives(create_widgets_in_current_frame, current_frame):
 if LIVES <= 0:
 zero_lives()
 else:
 current_frame.grid(column=0, row=0)
 create_widgets_in_current_frame
 def zero_lives():
 all_frames_forget()
 for i in range(1): #This is for the progress bar frame
 progress_bar_frame[i].grid_forget()
 special_frames[2].grid(column=0, row=0, sticky=(tkinter.W, tkinter.E))
 label_5 = Label(
 special_frames[2],
 text="You have no remaining lives. \nPlease quit the lesson and try again.",
 font=STANDARD_CONFIGS["font"]["title"])
 label_5.grid(columnspan=4, row=0)
 quit_button = Button(
 special_frames[2],
 text="Quit",
 command=root_window.destroy)
 quit_button.grid(column=1, columnspan=2, row=2)
 def quit_program_button(current_frame):
 quit_button = Button(
 current_frame,
 text="Quit",
 font=STANDARD_CONFIGS["font"]["bodytext"],
 command=quit_program)
 quit_button.grid(column=3, row=5)
 def quit_program():
 root_window.destroy()
 def next_button(frame_number):
 next_button = Button(
 special_frames[1],
 text="Next Question",
 command=lambda: next_question(frame_number))
 next_button.grid(column=0, columnspan=5, row=3)
 def next_question(frame_number):
 frame_number += 1
 progress_bar.step(1)
 call_frame(frame_number)
 def all_frames_forget():
 for i in range(0, no_of_quiz_questions): #This is for question frames
 frames[i].grid_remove()
 for i in range(0, 3): #This is for special frames: (correct and incorrect answer frames)
 special_frames[i].grid_remove()
 def create_widgets_function(frame_number):
 current_frame = frames[frame_number]
 question_number = random_list_of_questions[frame_number]
 question_frame_populator(frame_number, data, current_frame, question_number)
 def call_frame(frame_number):
 all_frames_forget()
 if frame_number == no_of_quiz_questions:
 print("Lesson complete")
 root_window.destroy()
 if frame_number != no_of_quiz_questions:
 create_widgets_in_current_frame = create_widgets_function(frame_number)
 current_frame = frames[frame_number]
 check_remaining_lives(create_widgets_in_current_frame, current_frame)
 ##### Program starts here #####
 image_path = f"{lesson_file_path}Images\\"
 sound_path = f"{lesson_file_path}Sounds\\"
 root_window = Toplevel() # Create the root GUI window.
 root_window.title(full_lesson_name) #This variable is passed from the main program
 random_list_of_questions = []
 for i in range(0, no_of_questions):
 random_question = f"question{str(i)}"
 random_list_of_questions.append(random_question)
 random.shuffle(random_list_of_questions) #This section randomizes the question order
 global SCORE
 SCORE = 0 #Setting the initial score to zero.
 global LIVES
 LIVES = 3 #Setting the initial number of lives.
 frame_number = 0
 frames = [] # This includes frames for all questions
 for i in range(0, no_of_quiz_questions):
 frame = tkinter.Frame(
 root_window,
 borderwidth=STANDARD_CONFIGS["borderwidth"],
 relief=STANDARD_CONFIGS["active"]["rel"])
 frame.grid(column=0, row=0, sticky=(tkinter.W, tkinter.N, tkinter.E))
 frames.append(frame)
 special_frames = [] #Includes the frames: wrong ans, right ans, zero lives, and progress bar
 for i in range(0, 4):
 special = tkinter.Frame(
 root_window,
 borderwidth=STANDARD_CONFIGS["borderwidth"],
 relief=STANDARD_CONFIGS["active"]["rel"])
 special.grid(column=1, row=0, sticky=(tkinter.W, tkinter.E))
 special.grid_forget()
 special_frames.append(special)
 progress_bar_frame = []
 for i in range(0, 1):
 test = tkinter.Frame(
 root_window,
 borderwidth=STANDARD_CONFIGS["borderwidth"],
 relief=STANDARD_CONFIGS["active"]["rel"])
 test.grid(column=0, row=1)#, sticky=(tkinter.W, tkinter.E))
 progress_bar_frame.append(test)
 progress_bar = ttk.Progressbar(
 progress_bar_frame[0],
 orient='horizontal',
 mode='determinate',
 maximum=no_of_quiz_questions)
 progress_bar.grid(column=0, row=1, columnspan=3)
 random.shuffle(frames)
 call_frame(frame_number) #Calls the first function which creates the firist frame
 root_window.mainloop()

If other problems become apparent during the review of my code please feel free to comment on them as well. I'm hoping to learn more about the use of global variables specifically but I open to any and all feedback that is offered. There are some images below of examples of the different types of questions being asked.

Multiple Choice Question:

enter image description here

Entry Box Question:

enter image description here

Sentence Re-Ordering Question: enter image description here

Matching Question:

enter image description here

Example of a question answered incorrectly: enter image description here

Example of a question answered correctly: enter image description here

asked Sep 2, 2019 at 15:51
\$\endgroup\$
1
  • \$\begingroup\$ As far as I know the general rule is "avoid global variables". Only used them for constants. If parts of your code require accessing common variables, then all those functions and variables should belong to a class. \$\endgroup\$ Commented Sep 2, 2019 at 16:03

2 Answers 2

3
\$\begingroup\$

It's clear that you're starting to pick up some good habits, but this code still has a way to go.

This is a comment

Module Docstring:

No need to write this - it's obvious from context.

Type hints

def __init__(self, sf, cho, noa):

Those parameters have no documentation, and it isn't clear what type they are. Do a Google for type hinting in Python - that will help, even if you don't end up adding a docstring for this function.

Class purpose

class CheckAnswers:

The name of this class on its own suggests that it isn't really being used correctly. "Check answers", as an action phrase, means that this would be better-suited to a method. At the least, this should be called AnswerCheck or AnswerChecker, nouns that imply objects instead of actions.

Dictionaries

This block:

if lesson_type == "quiz":
 no_of_quiz_questions = 8
if lesson_type == "short":
 no_of_quiz_questions = 4
if lesson_type == "conversation":
 no_of_quiz_questions = 1
if lesson_type == "long":
 no_of_quiz_questions = 8

is better-represented as a dictionary with a single lookup call.

Globals and state management

This boils down to object-oriented programming theory and how to manage program state. You're correct to identify that your usage of globals is not ideal. Taking a look at this code:

 global WORD_POSITION
 if question_directory["translationdirection"] == "EtoA":
 word_chosen[i].grid(column=size_of_word_bank-WORD_POSITION, row=1, columnspan=1)
 if question_directory["translationdirection"] == "AtoE":
 word_chosen[i].grid(column=WORD_POSITION+1, row=1, columnspan=1)
 WORD_POSITION += 1
 global USER_ANSWER
 USER_ANSWER.append(sentance_components[i])

You're modifying two globals. That means that this method is not written in the right context. It should probably be written as a method on a top-level Game class, and those two variables should be properties. Most of the time, class methods should only modify the state of their own class instance.

Nested functions

You've written a long series of nested functions in lesson_template. There's no clear need for this. They should be moved to global scope, or to class methods.

Loooooooong methods

question_frame_populator is way too long. Try to divide it up into logical sub-routintes.

Method names

sentancebuilder should be sentence_builder, or more appropriately build_sentence.

else

 if solution == USER_ANSWER:
 CheckAnswers(special_frames, None, None).right_answer()
 if solution != USER_ANSWER:
 CheckAnswers(special_frames, None, None).wrong_answer()

You can replace the second if with an else. This pattern is seen elsewhere.

answered Sep 2, 2019 at 16:13
\$\endgroup\$
3
\$\begingroup\$

Your global variable naming violates PEP 8. Names in all upper-case are constants; variables that never change in value. Your globals aren't constant though. Lines like

LIVES -= 1

change the value that LIVES holds.

Yes, global names should be in uppercase, but only because globals should also ideally only be constants. Global mutable state is a pain to deal with and complicates testing and debugging.


The simplest way to get rid of the global variables is to package them into a state that gets passed around to any function that needs it. While simple and not ideal, this solves the major problem with using globals: You can't just pass in data that you want to be used when testing. When using globals, you must modify the global state just to test how a function reacts to some data. This is less-straightforward than just passing the data, and has the potential to lead to situations where another function reads from the global that you set, causing it to change in behavior along with the function that you're testing.

So, how can you do this?

Represent the state of the game as a class (something like a dataclass would work well here, but I'm just going to use a plain class for simplicity).

class GameState:
 def __init__(self):
 self.lives = 3
 self.score = 0
 self.var = None # Bad name. Doesn't describe its purpose
 self.word_position = None
 self.user_answer = None

Then pass an instance of this object (and alter this object) to change the "globals". This at the very least allows you to easily pass in exactly the data that you want to test.



I'd review further, but I started feeling sick about half way through writing this. I'm going to go lay down :/

answered Sep 2, 2019 at 16:22
\$\endgroup\$
2
  • \$\begingroup\$ Thanks, this was a really helpful review and I've been working on fixing my use of global variables as a result! I'm marking the other answer as "correct" simply because it is more comprehensive but this was truly helpful. Hope you're feeling better now. \$\endgroup\$ Commented Sep 3, 2019 at 9:01
  • \$\begingroup\$ @JackDuane No problem. Glad to help. And it ended up being food poisoning. Seems to have resolved itself now. \$\endgroup\$ Commented Sep 3, 2019 at 13:04

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.