Electron Adventures: Episode 95: Pywebview

Pywebview staples together Python backend with OS-specific web engine frontend.

I'll say in advance that this is a total disaster. On many Windows machines you will literally get IE11 engine rendering your app. Even in best case scenario, you won't even have console.log available, and there's no reloading other than by quitting the whole app and restarting. Depending on not just the OS, but what's installed on the OS, you'll face completely different engine with completely different limitations, so developing anything nontrivial is going to be a huge pain. But for now, let's ignore all such issues.

Also Python situation with installing libraries is a lot messier than JavaScript or Ruby. I ran these on OSX 11.4, with pip3 install pywebview. If you have trouble installing that and following along, you'll need to refer to pywebview documentation.

hello1

We can start with the simplest possible program - just create a window passing a title and a URL

#!/usr/bin/env python3

import webview

window = webview.create_window(
  "Hello, World!",
  "https://en.wikipedia.org/wiki/%22Hello,_World!%22_program"
)
webview.start()

Here's the result:

electron-adventures-95a-screenshot.png

hello2

We can also generate HTML and send it into the browser window.

#!/usr/bin/env python3

import webview

html="""
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <style>
      body {
        margin: 0;
        display: flex;
        flex-direction: column;
        justify-content: center;
        align-items: center;
        background-color: #444;
        color: #fff;
        min-height: 100vh;
      }
    </style>
  </head>
  <body>
    <h1>Hello, World!</h1>
  </body>
</html>
"""

window = webview.create_window(
  "Hello, World!",
  html=html
)
webview.start()

Here's the result:

electron-adventures-95b-screenshot.png

hello3

Let's try another thing, loading from a file. Here's Python, HTML, and CSS parts.

Passing file: URLs dooesn't seem to work, but passing file paths directly does.

#!/usr/bin/env python3

import webview

window = webview.create_window(
  "Hello, World!",
  "hello3.html"
)
webview.start()

The document:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <link rel="stylesheet" href="./hello3.css" />
  </head>
  <body>
    <h1>Hello, World!</h1>
  </body>
</html>

The styling:

body {
  margin: 0;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  background-color: #444;
  color: #fff;
  min-height: 100vh;
}

Here's the result, identical to what we had before:

electron-adventures-95c-screenshot.png

Counter

Now that we went through the warm-up, let's write a click counter app.

We can create an API for the webapp and pass it as js_api argument. It will be available on the frontend through window.pywebview.api. It's important to note that it's completely async so we need to await all results.

#!/usr/bin/env python3

import webview

class App:
  def __init__(self):
    self.counter = 0

  def click(self):
    print("Clicked!")
    self.counter += 1

  def getCount(self):
    return self.counter

app = App()

window = webview.create_window(
  "Click Counter",
  "counter.html",
  js_api=App()
)
webview.start()

The document:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <link rel="stylesheet" href="./counter.css" />
  </head>
  <body>
    <div>Click count: <span id="count">0</span></div>
    <button>Click</button>
    <script src="./counter.js"></script>
  </body>
</html>

The styling:

body {
  margin: 0;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  background-color: #444;
  color: #fff;
  min-height: 100vh;
  font-size: 300%;
}
button {
  font-size: unset;
}

And finally the frontend code, notice all the awaits:

let button = document.querySelector("button")
let count = document.querySelector("#count")

button.addEventListener("click", async () => {
  await window.pywebview.api.click()
  count.innerText = await window.pywebview.api.getCount()
})

Here's the result:

electron-adventures-95d-screenshot.png

Conclusions

Pywebview staples together a nice backend - fully powered Python, and a disastrous frontend without even a console.log. It's something to consider if you have big existing Python codebase, you want to create a very simple frontend for it, and you know the systems it will run on, but it's grossly insufficient for anything requiring more complex frontends.

These are mostly technical limitations rather than anything fundamental, and with some effort pywebview could definitely be developed into a viable platform with minor changes (drop IE11, add dev tools, add reload etc.).

And even though I already concluded it's quite bad, in the next episode we'll do our traditional terminal app in pywebview anyway.

As usual, all the code for the episode is here.