Search code examples
pythondictionarycomboboxttkkeyerror

Is there a simpler way to write this code involving multiple comboboxes and nested dictionaries, and also avoid the KeyError?


I am rather new to Python, and after spending many hours I managed to get this working without having to ask a question, however, it definitely seems like it could be rewritten better and in a way that will get rid of this KeyError: ''. The key error only appears (and stalls the function) until I have chosen an item from each combobox, then it resumes due to my math in the function, but I can't figure out another way to write it that would resolve that issue and make the code more compact. I am sure there is a way, but I could definitely use a pointer in the right direction, thanks! Here is a simpler demonstration version of my program:

import tkinter as tk
import tkinter.ttk as ttk

#DICTIONARIES#
weapondict = {"Bronze Sword": {"atk":32, "def":4}, "Iron Sword": {"atk":47, "def":5}}
shielddict = {"Bronze Shield": {"atk":3, "def":10}, "Iron Shield": {"atk":5, "def":27}}

#FUNCTION#
def selected(func):
    a = weapondict[weaponvar.get()]["atk"]
    b = shielddict[shieldvar.get()]["atk"]
    atkvar.set(a + b)
    c = weapondict[weaponvar.get()]["def"]
    d = shielddict[shieldvar.get()]["def"]
    defvar.set(c + d)

#WINDOWLOOP#
root = tk.Tk()
root.geometry("250x125")

#VARIABLES#
weaponvar = tk.StringVar()
shieldvar = tk.StringVar()
atkvar = tk.IntVar()
defvar = tk.IntVar()

#COMBOBOXES#
weaponbox = ttk.Combobox(root, height=5, state="readonly", values=list(weapondict.keys()), textvariable=weaponvar)
weaponbox.place(x=10, y=10, width=130)
weaponbox.bind('<<ComboboxSelected>>', func=selected)

shieldbox = ttk.Combobox(root, height=5, state="readonly", values=list(shielddict.keys()), textvariable=shieldvar)
shieldbox.place(x=10, y=70, width=130)
shieldbox.bind('<<ComboboxSelected>>', func=selected)

#LABELS#
atklabel = tk.Label(root, text='Atk Bonus:')
atklabel.place(x=150, y=10, width=70, height=20)

deflabel = tk.Label(root, text='Def Bonus:')
deflabel.place(x=150, y=70, width=70, height=20)

atktotal = tk.Label(root, textvariable=atkvar)
atktotal.place(x=155, y=30, width=100, height=20)

deftotal = tk.Label(root, textvariable=defvar)
deftotal.place(x=155, y=90, width=100, height=20)

root.mainloop()

The goal is simply to take a selection from each combobox and take it's specified value, add that integer to the other one to give a total, while resolving the keyerror issue and making the code more readable and easier to edit. I want to put multiple boxes, each with multiple items and it will get very messy with this approach, thank you in advance!


Solution

  • The problem is the tk.StringVar get() method calls will return "" when there's nothing in them. A simple fix would be to add an entry to both of the dictionaries that would match this "empty" key and give them some associated values to be used when nothing is selected (i.e. zero) in the corresponding Combobox:

    #DICTIONARIES#
    weapondict = {"": {"atk":0, "def":0},  # Use when nothing is selected.
                  "Bronze Sword": {"atk":32, "def":4},
                  "Iron Sword": {"atk":47, "def":5}}
    shielddict = {"": {"atk":0, "def":0},  # Use when nothing is selected.
                  "Bronze Shield": {"atk":3, "def":10},
                  "Iron Shield": {"atk":5, "def":27}}
    

    An alternative would be to modify the selected() function to check whether the values returned by weaponvar.get() and shieldvar.get() were keys in the corresponding dictionary before trying to use them. If they aren't, then some default values for a, b, c, or d could then be provided.

    That's completely feasible, but changing the two dictionaries seems a lot easier IMO.

    Also note how the two nested dictionaries are defined. I find doing it on multiple lines to be a lot more readable.