3
\$\begingroup\$

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:]

def adjust_adjacent_parentheses_int(s):
    """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):
            result = adjust_adjacent_parentheses_int(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
btn_padding = 1
border_width = 3
tk.Button(master=main_btn_frm, text="1", fg="black", bg="orange", relief=tk.RAISED, bd=border_width, command=partial(insert_value, "1")).grid(row=0, column=0, padx=btn_padding, pady=btn_padding, sticky="nsew")
tk.Button(master=main_btn_frm, text="2", fg="black", bg="orange", relief=tk.RAISED, bd=border_width, command=partial(insert_value, "2")).grid(row=0, column=1, padx=btn_padding, pady=btn_padding, sticky="nsew")
tk.Button(master=main_btn_frm, text="3", fg="black", bg="orange", relief=tk.RAISED, bd=border_width, command=partial(insert_value, "3")).grid(row=0, column=2, padx=btn_padding, pady=btn_padding, sticky="nsew")
tk.Button(master=main_btn_frm, text="+", fg="black", bg="orange", relief=tk.RAISED, bd=border_width, command=partial(insert_value, "+")).grid(row=0, column=3, padx=btn_padding, pady=btn_padding, sticky="nsew")
tk.Button(master=main_btn_frm, text="4", fg="black", bg="orange", relief=tk.RAISED, bd=border_width, command=partial(insert_value, "4")).grid(row=1, column=0, padx=btn_padding, pady=btn_padding, sticky="nsew")
tk.Button(master=main_btn_frm, text="5", fg="black", bg="orange", relief=tk.RAISED, bd=border_width, command=partial(insert_value, "5")).grid(row=1, column=1, padx=btn_padding, pady=btn_padding, sticky="nsew")
tk.Button(master=main_btn_frm, text="6", fg="black", bg="orange", relief=tk.RAISED, bd=border_width, command=partial(insert_value, "6")).grid(row=1, column=2, padx=btn_padding, pady=btn_padding, sticky="nsew")
tk.Button(master=main_btn_frm, text="-", fg="black", bg="orange", relief=tk.RAISED, bd=border_width, command=partial(insert_value, "-")).grid(row=1, column=3, padx=btn_padding, pady=btn_padding, sticky="nsew")
tk.Button(master=main_btn_frm, text="7", fg="black", bg="orange", relief=tk.RAISED, bd=border_width, command=partial(insert_value, "7")).grid(row=2, column=0, padx=btn_padding, pady=btn_padding, sticky="nsew")
tk.Button(master=main_btn_frm, text="8", fg="black", bg="orange", relief=tk.RAISED, bd=border_width, command=partial(insert_value, "8")).grid(row=2, column=1, padx=btn_padding, pady=btn_padding, sticky="nsew")
tk.Button(master=main_btn_frm, text="9", fg="black", bg="orange", relief=tk.RAISED, bd=border_width, command=partial(insert_value, "9")).grid(row=2, column=2, padx=btn_padding, pady=btn_padding, sticky="nsew")
tk.Button(master=main_btn_frm, text="×", fg="black", bg="orange", relief=tk.RAISED, bd=border_width, command=partial(insert_value, "×")).grid(row=2, column=3, padx=btn_padding, pady=btn_padding, sticky="nsew")
tk.Button(master=main_btn_frm, text="0", fg="black", bg="orange", relief=tk.RAISED, bd=border_width, command=partial(insert_value, "0")).grid(row=3, column=0, padx=btn_padding, pady=btn_padding, sticky="nsew", columnspan=2)
tk.Button(master=main_btn_frm, text=".", fg="black", bg="orange", relief=tk.RAISED, bd=border_width, command=partial(insert_value, ".")).grid(row=3, column=2, padx=btn_padding, pady=btn_padding, sticky="nsew")
tk.Button(master=main_btn_frm, text="÷", fg="black", bg="orange", relief=tk.RAISED, bd=border_width, command=partial(insert_value, "÷")).grid(row=3, column=3, padx=btn_padding, pady=btn_padding, sticky="nsew")
tk.Button(master=main_btn_frm, text="(", fg="black", bg="orange", relief=tk.RAISED, bd=border_width, command=partial(insert_value, "(")).grid(row=4, column=0, padx=btn_padding, pady=btn_padding, sticky="nsew")
tk.Button(master=main_btn_frm, text="C", fg="black", bg="red", relief=tk.RAISED, bd=border_width, command=clear_result).grid(row=4, column=1, padx=btn_padding, pady=btn_padding, sticky="nsew")
tk.Button(master=main_btn_frm, text="=", fg="black", bg="green", relief=tk.RAISED, bd=border_width, command=calculate_result).grid(row=4, column=2, padx=btn_padding, pady=btn_padding, sticky="nsew")
tk.Button(master=main_btn_frm, text=")", fg="black", bg="orange", relief=tk.RAISED, bd=border_width, command=partial(insert_value, ")")).grid(row=4, column=3, padx=btn_padding, pady=btn_padding, sticky="nsew")

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

#main loop
window.mainloop()
\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

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.
  • Capitalize your global constants.
  • 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:]


def adjust_adjacent_parentheses_int(s: str) -> str:
    """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
        btn_padding = 1
        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,
                padx=btn_padding,
                pady=btn_padding,
                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):
                result = adjust_adjacent_parentheses_int(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()
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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