Electron Adventures: Episode 95: Pywebview
Tags: javascript, electron, python, qt
Published: 2021-11-12 14:20:13

[Pywebview](https://pywebview.flowrl.com/) staples together Python backend with [OS-specific web engine frontend](https://pywebview.flowrl.com/guide/renderer.html).

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

```python
#!/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:

![Episode 95 Screenshot A](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qkm55fawd4bf7pjjp5w3.png)

### `hello2`

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

```python
#!/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:

![Episode 95 Screenshot B](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kjw5rd0c386b9yiwd5sb.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.

```python
#!/usr/bin/env python3

import webview

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

The document:

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

The styling:

```css
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:

![Episode 95 Screenshot C](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4n3fmbmaucytog3jcrzy.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.

```python
#!/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:

```html
<!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:

```css
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 `await`s:

```javascript
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:

![Episode 95 Screenshot D](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/danaqdo2ba185x5r3c59.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](https://github.com/taw/electron-adventures/tree/master/episode-95-pywebview).