Build a GUI and package 📦 your killer Python scripts 📜with Tkinter and Pyinstaller
Tags: python, gui, build, packaging
Published: 2023-01-24 12:22:41

I loooove the CLI! 😍😍
I can safely assume that you all agree with me when I say, it's fast and efficient. After all, the IT tech people 👨‍💻 are spending almost all of their time either using CLI tools/scripts or writing them 😉.

In my line of work, I write a lot of Python scripts and CLI tools and I'm very happy using them as they are 😊!
But every now and then, you HAVE to build a GUI for your killer script and build and executable for it to widen its users base (if that's something you aim for).

Like one time, I've written a Python script that processes scanned images of a survey that uses "bubbles" as their user's responses and puts them in a csv file for further analysis. With just one command in the terminal, I could process hundreds of images just like magic 🧙‍♂️.

I had only one problem, the users that are supposed to collect the surveys and process them don't know what a "terminal" is so they neither could create the necessary environment to run the script nor executing the script via CLI (duh!) 🤦‍♂️. 

So ultimately, I had to revisit Tkinter - Python's native GUI module - to create a GUI for my scripts and to check out Pyinstaller as a final step to package the whole thing and then ship it the end users.

## First of, Tkinter:
Prior to version 8.5, Tkinter generated GUI was ... very bad 🤢! It looked very 1990's! I didn't consider it much and often looked for alternatives 🤷‍♂️. But now after v8.5, it - as written in the changelog - "makes use of platform-specific theming". Which means it will inherit whatever theme the OS is using. It isn't cutting-edge but still way better from older versions 🤩.

If you didn't work with Tkinter before, it might look intimidating at first but it's very intuitive once you get its pattern:
1. You initiate your widget (in Tkinker, controls are called "widgets").
2. You place your widget.
That's it 😉! 

### Tkinter app anatomy:
* A Tkinter app always starts with either a Frame (container for other controls) or a Tk instance (which is technically a frame).
```python
form tkinter import Tk, Label, Button, StringVar

root = Tk()
root.title("My cool app")
root.geometry("400x200+100+100") # Width x Hieght + x + y
```
* To start your app, you have to call and endless loop so the application is always responding to your interactions with it.
```python
root.mainloop()
```
* Widgets have a set of default parameters that you can set across the majority of them. Things like "width", "height", "text", "title", ..., etc.
```python
my_label = Label(
    root,           # The parent of the widget
    text="Hi this is my label!",
    width=40
)
```
* To place a widget inside its container, you either use grid() or pack() but not both in the same container.
```python
my_label.pack(side="top")       # No need to specify the exact position
# or
my_label.grid(row=0, column=0)  # Grid transforms the container into a grid and you specify the row and column placement.
```
* A Button widget can take a "command" parameter that refers to the function/method it executes. If this function/method takes parameters of its own, you can wrap inside a "lambda" function.
```python
my_button = Button(
    root,
    text="Press Me",
    command=lambda: print("Hello World!")
)
my_button.pack()
```
* In Tkinter, there are classes representing some python types like StringVar, IntVar, ..., etc. You can set/get their values with their ... set() and get() methods 😁. One benefit of using those, is that you can dynamically change the values of some widgets properties like a Label's "text" or Progressbar's "value".
```python
from tkinter import Tk, StringVar, Label, Button

root = Tk()
root.geometry("200x200")
root.title("Counter")


def increase_count():
    # A function that will be executed by the button
    count = int(count_var.get())
    count += 1
    count_var.set(str(count))

# StringVar initiation
count_var = StringVar(value="0")

label_count = Label(
    root,
    textvariable=count_var  # Binding the StringVar with the label
)
label_count.pack()

button_count = Button(
    root,
    text="Increase Count",
    command=increase_count  # Binding the function withe button
)
button_count.pack()

if __name__ == "__main__":
    root.mainloop()
```
The previous code snippet will produce the following result:

![code snippet result](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rikyxlikolvam0s9qodr.gif)
* If you used OOP (Object Oriented Programming) modeling to write your GUI app, you will save yourself from a LOT of trouble 😉. We can rewrite the previous example as follow:
```python
from tkinter import Tk, StringVar, Label, Button


class MyApp(Tk):
    def __init__(self, title, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self.geometry("200x200")
        self.title(title)
        self.counter = StringVar(value="0")

        self.creat_gui()

    def creat_gui(self):
        label_count = Label(
            self,
            textvariable=self.counter   # Binding the StringVar with the label
        )
        label_count.pack()

        button_count = Button(
            self,
            text="Increase Count",
            command=self.increase_count  # Binding the method withe button
        )
        button_count.pack()

    def increase_count(self):
        count = int(self.counter.get())
        count += 1
        self.counter.set(str(count))


my_cool_app = MyApp(title="Counter")

if __name__ == "__main__":
    my_cool_app.mainloop()
```

You can find a lot more details and examples in the official Tkinter [docs](https://docs.python.org/3/library/tkinter.html) 👌

## Secondly, packaging with Pyinstaller:
I wished that I could write a lot of cool code snippets in this section but Pyinstaller is so simple that you can package your whole app with just the following command 😁:
```bash
pyinstaller myapp.py
```
And assuming everything goes well, you will find your executable in the ./dist directory. But with Pyinstaller, there are more than meets the eye 😉. For example, you can set the executable icon, hide the console window (it's shown by default) and generate one file executable on a windows machine with this command:
```bash
pyinstaller myapp.py --noconsole --icon ./myicon.ico --onefile
```
There are lots of cool options for Pyinstaller that you can learn all about them from the official [docs](https://pyinstaller.org/en/stable/) 🤩.

# Finally
If you are eager to see a complete example on a Tkinter gui, I've created and example which is more or less simulating the actual situation which I told you about earlier in this post. You can find it in this [github repo](https://github.com/FadyGrAb/TechNuggets/tree/main/02-gui-and-packaging-python). I've separated my business logic (killer_script.py) from my GUI (myapp.py) and I used a launcher module (app_launcher.py) for further modularity. 
I did my best commenting the py files 🙂 but you will always find something that I missed and you didn't totally understand! That's completely normal. You only have to look it up online or in the docs! If that didn't work, just drop it below in the comments and I'll try my best to make it simpler for you 😉