How to Create Better UI for Your Python Scripts

Howard Tucan
The Startup
Published in
6 min readJan 22, 2021

As time goes by, more and more of my non-developer friends are seeing code creep into their own line of work. It makes a lot of sense — I’ve long been hearing about long manual processes that yearn for automation and finally the cat is out of the bag. People are starting to realise that programming need not be the reserve of basement dwelling introverts, but is in fact increasingly accessible to all.

As intelligent folk, they’re able to write a script that does a bit of work for them, but it tends to stay exactly that — a script, and they end up with different scripts for different tasks. This tutorial is aimed at those — the people starting to leverage the benefits of python, but wanting to get more from their scripts.

For illustration purposes, lets consider the following example, which takes a phrase and jumbles the letters around just for fun.

message = "heLlo wOrld!"AtoZ = range(ord('A'), ord('Z')+1)
atoz = range(ord('a'), ord('z')+1)
rotate = 13
output = []
for letter in message:
ascii_val = ord(letter)
start = None
if ascii_val in AtoZ:
start = AtoZ[0]
elif ascii_val in atoz:
start = atoz[0]
if start:
alpha = ord(letter)-start
rotated = (alpha+rotate)%26
output.append(chr(start+rotated))
else:
output.append(letter)
print("".join(output))

On execution, all this does is rotate the letters in the message variable and then log out the result, which should be “urYyb jBeyq!”. Great! If our goal was to jumble this one string alone, we are done. Realistically, we probably have a whole bunch of strings we want to jumble - but right now, our script is only really capable of jumbling one string at a time, so lets fix that:

def jumble(message,rotate=13):
AtoZ = range(ord('A'), ord('Z')+1)
atoz = range(ord('a'), ord('z')+1)
output = [] for letter in message:
ascii_val = ord(letter)
start = None
if ascii_val in AtoZ:
start = AtoZ[0]
elif ascii_val in atoz:
start = atoz[0]
if start:
alpha = ord(letter)-start
rotated = (alpha+rotate)%26
output.append(chr(start+rotated))
else:
output.append(letter)
return "".join(output)result = jumble("heLlo wOrld!")
print(result)
print(jumble(result))

This is an improvement. Now we have created a function to do the work, we can call it multiple times (here we’re converting the string once, and then converting it back again), but we’re still relying on modifications to the script every time you want to jumble up a new message. Note that we also made the rotate value a parameter, but defaulted it to the value we want for now — that will make life easier later if we decide we want to offset the letters by a different number.

The easiest way of having a script you can reuse without having to keep modifying the source, would be to pass some parameters via the command line and use sys.argv to collect the values, but we’re going to bypass that in favour of creating interfaces that anyone can use without resorting to the command line.

Firstly, lets look at TKinter. TKinter is provided out of the box so there’s no additional dependencies to worry about, and its nice and portable. Lets add this block to the end of our script:

if __name__ == "__main__":
import tkinter as tk
def dojumble():
res.configure(text=entry.get()+" -> "+jumble(entry.get()))
w = tk.Tk()
lbl = tk.Label(w, text="Enter your message for jumbling below…")
lbl.pack(pady=10)
entry = tk.Entry(w)
entry.pack(pady=10)
tk.Button(w, text="Jumble", command=dojumble).pack(pady=10)
res = tk.Label(w)
res.pack(pady=10)
w.mainloop()

So what does this do? Well firstly, we’ve added this line:

if __name__ == "__main__":

If you’ve ever seen this before and wondered what it meant, it just tells the interpreter to run this next block of code if our script is being ran directly, but to ignore it if we’re being imported from elsewhere (this leaves the door open to being able to use the same py file on another project, without having to make any changes). Most of the remaining code we’ve added is to populate a window with some content — a label, to tell a user what to do, a text entry field, to enter our message, a button, to activate the feature, and a label to show the result in. There a various means of laying out the window, but here we are just relying on a useful default “packing” albeit with a bit of Y padding to space things out a little. The mainloop() command at the end starts the application, and you we see something like this:

This is how it might look in windows — on other platforms it will appear accordingly to match the desktop environment.

Our script is now behaving more like an application and will hang around until we choose to close it by clicking the “X” at the top. Clicking the “Jumble” button has been wired to take the current value of the entry field, and send it to our existing jumble method, after which it will stick the result into the second label. Easy peasy!

OK, that’s pretty cool — but what if you want a colleague to use your app? You still have to send them your script and help them install python and so on, or build your app into a fully fledged executable to share (perhaps a topic for a future tutorial?). But what if we could expose this functionality to colleagues, without them having to install or run anything, safe in the knowledge that they were always using the latest version of your tool?

Enter CherryPy. CherryPy is a minimalist web framework that’s going to make it easy for us to expose our function over http and make it possible to share our cool jumble function with others on the same network. Unlike TKinter, CherryPy isn’t a standard module, so we need to install that using pip:

python pip install cherrypy

Then lets replace our __main__ section from before with this:

if __name__ == "__main__":
import cherrypy
import os
class Jumbler(object):
@cherrypy.expose
def jumble(self, message):
return jumble(message)
cherrypy.config.update( {
"server.socket_host": "0.0.0.0",
"server.socket_port": 9090,
} )
cherrypy.quickstart(Jumbler())

Feel free to change the port to whatever suits. Now when you run your script, there’s not really anything to see (if you haven’t accessed the network before from a python script, you might get asked if its ok for python to go through the firewall). However, what this code above has done is expose the “jumble” function on port 9090. Try visiting:

http://localhost:9090/jumble?message=hello 

in the browser — hopefully you will get your jumbled message back and shown in the page. Congratulations, you have just created the world’s smallest API!

Clearly, this isn’t the best “user interface” but what if we could leverage the flexibility and portability of the web? We’re now only a stones throw away. Lets replace the very last line with the following block:

conf = {
"/": {
"tools.staticdir.on": True,
"tools.staticdir.dir": os.path.dirname(os.path.abspath(__file__)),
"tools.staticdir.index": "index.html"
}
}
cherrypy.quickstart(Jumbler(), config=conf)

What this does is allow you to place an index.html file next to your .py file and then serve it on localhost:9090. I’m not going to get into the details of HTML and JavaScript here, but this arrangement means you can create a web based user interface to your python app, while neatly separating the frontend from the logic. This is what a very basic UI might look like:

<!DOCTYPE html>
<html>
<body style="background-color: beige;">
<p>Enter your string to jumble:</p>
<input type="text" id="toJumble">
<button type="button" onclick="doJumble()">Jumble</button>
<p id="result"></p>
<script>
function doJumble() {
var x = document.getElementById("toJumble");
fetch("/jumble?message="+x.value).then(
(response) => response.text().then(
(text) => {
let el = document.getElementById("toJumble");
let result = el.value+" -> "+text;
document.getElementById("result").innerHTML=result;
}
)
);
}
</script>
</body>
</html>

and the result:

Now you can get up from your desk and use your app from your phone*! I hope this rough guide helps or at least inspires someone!

*Assuming your computer and phone are connected to the same network.

--

--

Howard Tucan
The Startup

20+ years of programming experience and still clueless.