# Tkinter Basic Calculator

This is my first project using the tkinter library. It's a basic calculator that supports the four basic arithmetic operations. It also supports inputting digits and symbols via the keyboard.

I'm looking for any sort of feedback. Are my comments sufficient for understanding the algorithm? Are there redundant comments or code? Is the program written well overall?

import tkinter as tk
from functools import partial

def insert_value(value):
"""Insert value into input entry widget
if value is an operator, add spacing on each side of it

value: value to add to widget"""

global acceptable_inputs, interchangeable_operators

value = convert_to_int(value)
cur_text = str(result_lbl["text"])

if cur_text == "Error":
result_lbl["text"] = f"{value}"
elif isinstance(value, int) or len(cur_text) == 0 or value in ("(", ")", "."): #don't add additional spacing
result_lbl["text"] = f"{cur_text}{value}"
else: #add spacing in between provided value (only if value is an arithmetic operator)
for s in interchangeable_operators:
if s[1] in value:
value = replace_operators(value, s[1], s[0]) #change typed "/" to "÷" and/or "*" to "×"
result_lbl["text"] = f"{cur_text} {value} "

def convert_to_int(d):
"""Convert data to integer; returns provided data as integer if the data can be converted; else returns unmodified data

d: data to convert"""

try:
return int(d)
except ValueError:
return d

def insert_in_string(s, index, value):
"""Insert value into string at index

s: string
index: index of insertion
value: value to insert into string"""

return s[:index] + value + s[index:]

"""Check if provided string contains adjacent integer and parenthesis, if it does, insert an astrik in between them; returns modified string

s: string"""

#find indices where astrik should be inserted
indices = []
for i in range(len(s)):
if isinstance(convert_to_int(s[i]), int):
if i+1 < len(s) and s[i+1] == "(": #if ( is immediately after an integer, plan to insert astrik
indices.append(i+1)
elif i-1 >= 0 and s[i-1] == ")": #if ) is immediately before an integer, plan to insert astrik
indices.append(i)

#insert astriks in their respective indices
if indices:
for i in range(len(indices)):
s = insert_in_string(s, indices[i]+i, "*")

return s

def replace_operators(s, symbol, operator):
"""Change symbol in provided string to its corresponding operator; returns modified string

s: string
symbol: symbol to change
operator: operator to replace with symbol"""

return s.replace(symbol, operator)

def calculate_result():
"""Print calculated result into entry widget"""

global parentheses, interchangeable_operators

result = str(result_lbl["text"])

if not result:
return
else:
result_lbl["text"] = ""

#modify string so it can be appropriately evaluated by the eval() function
for s in interchangeable_operators:
if s[0] in result:
result = replace_operators(result, s[0], s[1])

#insert astriks in between parentheses and integers
if any(c in parentheses for c in result):

try:
result = eval(result)
except (SyntaxError, NameError) as e:
result_lbl["text"] = "Error"
return

#convert evaluation to integer form if the float only has trailing 0s
if isinstance(result, float) and result-int(result) == 0:
result = convert_to_int(result)

result_lbl["text"] = result

def key_bindings(key):
"""Controller for executing corresponding functions related to key input. This function is binded to <Key> from tkinter

key: key object from tkinter binding"""

global acceptable_inputs

if key.char == "\r": #enter key was pressed, calculate expression currently in input
calculate_result()
elif key.keysym == "BackSpace": #delete latest character
cur_text = str(result_lbl["text"])
result_lbl["text"] = cur_text[:len(cur_text)-1]
elif key.char in acceptable_inputs: #insert character from key press into input bar if character is eligible
insert_value(key.char)

def clear_result():
"""Clear all characters contain within the result entry widget"""

result_lbl["text"] = ""

#initialize variables
interchangeable_operators = (("÷", "/"), ("×", "*"))
acceptable_inputs = ("+", "-", "/", "*", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", ".", "(", ")")
parentheses = ("(", ")")

#initialize window
window = tk.Tk()
window.title("Basic Calculator")
window.geometry("320x180")

#result frame/entry setup
result_frm = tk.Frame(master=window)
result_frm.pack(fill=tk.BOTH, expand=True)

result_lbl = tk.Label(text="", master=result_frm, fg="black", relief=tk.FLAT)
result_lbl.pack(expand=True)

#setup primary button grid/frame
main_btn_frm = tk.Frame(master=window, bg="#CAC8AE")
main_btn_frm.pack(fill=tk.BOTH, expand=True)

main_btn_frm.rowconfigure([0, 1, 2, 3], weight=1)
main_btn_frm.rowconfigure(4, weight=2)
main_btn_frm.columnconfigure([0, 1, 2, 3], weight=1)

#initialize all buttons inside of grid
border_width = 3

#binding(s)
window.bind("<Key>", key_bindings)

#main loop
window.mainloop()


First: despite running arbitrary code off of the internet being a Bad Idea, I did, and it works! So congratulations.

As with most beginner Tk code, this is a big hash of global setup code and global widget references. That needs to change. Some of your functions, namely

• convert_to_int
• insert_in_string
• adjust_adjacent_parentheses_int
• replace_operators

should stay global, and basically the rest of the code should be moved into functions or class methods.

Other bits:

• "astrik" is spelled "asterisk".
• Add a main guard at the bottom.
• Add some type hints. This is difficult for convert_to_int since it would technically work for inputs of str, int or float, and may return any of those - so you would need some large Unions. However, this speaks less to the convenience of the type hint system and more to the fact that this isn't a very safe way to go about passing data around.

The button setup is a mess. DRY - don't repeat yourself - and make an intermediate function and some loops to help you out.

## Suggested

import tkinter as tk
from functools import partial

# initialize variables
from typing import Union, Callable, Optional

INTERCHANGEABLE_OPS = (("÷", "/"), ("×", "*"))
GOOD_INPUTS = (
"+", "-", "/", "*",
"1", "2", "3", "4", "5", "6", "7", "8", "9", "0",
".", "(", ")",
)
PARENS = ("(", ")")

def convert_to_int(d):
"""Convert data to integer; returns provided data as integer if the data can be converted; else returns unmodified data

d: data to convert"""

try:
return int(d)
except ValueError:
return d

def insert_in_string(s: str, index: int, value: str) -> str:
"""Insert value into string at index

s: string
index: index of insertion
value: value to insert into string"""

return s[:index] + value + s[index:]

"""Check if provided string contains adjacent integer and parenthesis, if it does, insert an astrik in between them; returns modified string

s: string"""

# find indices where asterisk should be inserted
indices = []
for i in range(len(s)):
if isinstance(convert_to_int(s[i]), int):
if i + 1 < len(s) and s[i + 1] == "(":  # if ( is immediately after an integer, plan to insert astrik
indices.append(i + 1)
elif i - 1 >= 0 and s[i - 1] == ")":  # if ) is immediately before an integer, plan to insert astrik
indices.append(i)

# insert asterisks in their respective indices
if indices:
for i in range(len(indices)):
s = insert_in_string(s, indices[i] + i, "*")

return s

def replace_operators(s: str, symbol: str, operator: str) -> str:
"""Change symbol in provided string to its corresponding operator; returns modified string

s: string
symbol: symbol to change
operator: operator to replace with symbol"""

return s.replace(symbol, operator)

class Window:
def __init__(self):
# initialize window
self.window = tk.Tk()
self.window.title("Basic Calculator")
self.window.geometry("320x180")

# result frame/entry setup
result_frm = tk.Frame(master=self.window)
result_frm.pack(fill=tk.BOTH, expand=True)

self.result_lbl = tk.Label(text="", master=result_frm, fg="black", relief=tk.FLAT)
self.result_lbl.pack(expand=True)

# setup primary button grid/frame
main_btn_frm = tk.Frame(master=self.window, bg="#CAC8AE")
main_btn_frm.pack(fill=tk.BOTH, expand=True)

main_btn_frm.rowconfigure([0, 1, 2, 3], weight=1)
main_btn_frm.rowconfigure(4, weight=2)
main_btn_frm.columnconfigure([0, 1, 2, 3], weight=1)

# initialize all buttons inside of grid
border_width = 3

def button(text: str, row: int, col: int, colspan=1, **kwargs) -> tk.Button:
button_args = {
'text': text,
'command': partial(self.insert_value, text),
'master': main_btn_frm,
'fg': 'black',
'bg': 'orange',
'relief': tk.RAISED,
'bd': border_width,
}
button_args.update(kwargs)

return tk.Button(**button_args).grid(
row=row,
column=col,
columnspan=colspan,
sticky="nsew",
)

for row, syms in enumerate((
'123+',
'456-',
'789×',
)):
for col, sym in enumerate(syms):
button(sym, row, col)

button('0', 3, 0, colspan=2)
button('.', 3, 2)
button('÷', 3, 3)
button('(', 4, 0)
button('C', 4, 1, command=self.clear_result, bg='red')
button('=', 4, 2, command=self.calculate_result, bg='green')
button(')', 4, 3)

# binding(s)
self.window.bind("<Key>", self.key_bindings)

def insert_value(self, value: str):
"""Insert value into input entry widget
if value is an operator, add spacing on each side of it

value: value to add to widget"""

value = convert_to_int(value)
cur_text = str(self.result_lbl["text"])

if cur_text == "Error":
self.result_lbl["text"] = f"{value}"
elif isinstance(value, int) or len(cur_text) == 0 or value in ("(", ")", "."):  # don't add additional spacing
self.result_lbl["text"] = f"{cur_text}{value}"
else:  # add spacing in between provided value (only if value is an arithmetic operator)
for s in INTERCHANGEABLE_OPS:
if s[1] in value:
value = replace_operators(value, s[1], s[0])  # change typed "/" to "÷" and/or "*" to "×"
self.result_lbl["text"] = f"{cur_text} {value} "

def calculate_result(self):
"""Print calculated result into entry widget"""

result = str(self.result_lbl["text"])

if not result:
return
else:
self.result_lbl["text"] = ""

# modify string so it can be appropriately evaluated by the eval() function
for s in INTERCHANGEABLE_OPS:
if s[0] in result:
result = replace_operators(result, s[0], s[1])

# insert astriks in between parentheses and integers
if any(c in PARENS for c in result):

try:
result = eval(result)
except (SyntaxError, NameError):
self.result_lbl["text"] = "Error"
return

# convert evaluation to integer form if the float only has trailing 0s
if isinstance(result, float) and result - int(result) == 0:
result = convert_to_int(result)

self.result_lbl["text"] = result

def key_bindings(self, key: tk.Event):
"""Controller for executing corresponding functions related to key input. This function is binded to <Key> from tkinter

key: key object from tkinter binding"""

if key.char == "\r":  # enter key was pressed, calculate expression currently in input
self.calculate_result()
elif key.keysym == "BackSpace":  # delete latest character
cur_text = str(self.result_lbl["text"])
self.result_lbl["text"] = cur_text[:len(cur_text) - 1]
elif key.char in GOOD_INPUTS:  # insert character from key press into input bar if character is eligible
self.insert_value(key.char)

def clear_result(self):
"""Clear all characters contain within the result entry widget"""

self.result_lbl["text"] = ""

def run(self):
# main loop
self.window.mainloop()

if __name__ == '__main__':
Window().run()