Documentation

Build desktop apps with the web stack you already know

Glue hosts a local window, then lets Python and JavaScript call each other. No Electron ceremony. No new UI framework.

Install

PyPI project name is glue-ui. You still import glue in Python.

pip install glue-ui

Optional extras:

pip install "glue-ui[jinja2]"   # Jinja2 templates
pip install "glue-ui[build]"    # PyInstaller for packaging
pip install "glue-ui[splash]"   # Transparent PNG/APNG/GIF startup splash

From a clone: pip install . or editable pip install -e .

pip install "git+https://github.com/alepodj/Glue.git@main"

Quick start

Put your frontend in a folder named ui/ with index.html (the defaults), then:

import glue

glue.init()    # Override ui/ with any path: glue.init('frontend')
glue.start()   # Override index.html: glue.start('main.html')

By default Glue opens a PyWebView native window. If that isn’t available, it falls back to Chrome/Chromium in app mode (--app), then Edge on Windows only.

Include the bridge on every HTML page:

<script type="text/javascript" src="/glue.js"></script>

Backend layout is yours — one Python file or many folders. Glue does not require a fixed backend structure.

Call both ways

Python → available in JavaScript

@glue.expose
def add(a, b):
    return a + b
let sum = await glue.add(1, 2)();

JavaScript → available in Python

glue.expose(js_random);
function js_random() {
  return Math.random();
}
n = glue.js_random()()          # wait for the value
glue.js_random()(print)         # or use a callback

Complex values travel as JSON over a WebSocket. If a bundler renames functions, expose with an explicit name: glue.expose(fn, "my_name").

Hello, World!

Full example lives in examples/01 - hello_world on GitHub.

ui/index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Hello, World!</title>
    <script src="/glue.js"></script>
    <script src="/app.js" defer></script>
  </head>
  <body>Hello, World!</body>
</html>

ui/app.js

glue.expose(say_hello_js);
function say_hello_js(source) {
  console.log(`Hello from ${source}`);
}

say_hello_js("JavaScript World!");
glue.say_hello_py("JavaScript World!");

hello_world.py

import glue

glue.init()

@glue.expose
def say_hello_py(x):
    print('Hello from %s' % x)

say_hello_py('Python World!')
glue.say_hello_js('Python World!')
glue.start()

Return values

Callback

glue.js_random()(lambda n: print('Got', n))

Synchronous wait (after glue.start(), or with block=False)

n = glue.js_random()()

In JavaScript:

let n = await glue.py_random()();

Python waits up to 10 seconds for a JS result by default (js_result_timeout on glue.init).

Settings

Optional durable JSON for themes and app state. Python only — expose wrappers if the UI needs them.

import glue

glue.init(app_name='myapp')   # unlocks ~/.myapp/myapp.json

data = glue.settings()        # {} if missing — nothing created yet
data['theme'] = 'dark'
glue.save_settings(data)      # first save creates the folder + file
  • glue.settings_path() returns the default path when app_name is set.
  • Passing app_name alone does not create files.

Hosts

Default order for mode='auto':

  1. PyWebView — native window
  2. Chrome/Chromium — --app
  3. Edge — Windows only if Chrome is missing
modeBehavior
'auto'PyWebView → Chrome → Edge (Windows)
'webview'PyWebView only
'chrome' / 'edge'Force that browser
NoneServer only (tests / custom frontends)
'custom'Raw Popen(cmdline_args)

When ui/favicon.ico exists, Glue injects a favicon link into served HTML for Chrome/Edge captions. Full window options: API · start().

Async Python

Prefer glue.sleep() and glue.spawn() over time.sleep(). Monkey-patch gevent before importing Glue if needed.

from gevent import monkey
monkey.patch_all()
import glue

glue.init()

def ticker():
    while True:
        print("tick")
        glue.sleep(1.0)

glue.spawn(ticker)
glue.start(block=False)

With block=False, mode='auto' skips PyWebView and uses Chrome/Edge.

Package with PyInstaller

  1. Clean virtualenv
  2. pip install "glue-ui[build]"
  3. python -m glue your_script.py ui
  4. Ship from dist/; add --onefile --noconsole when ready

Project layout

my_app.py
ui/
  index.html
  css/
  js/

Security

By default Glue binds to localhost and only accepts WebSocket clients from loopback. Every @glue.exposed function is callable by any page that can open that socket.

  • Do not put secrets under ui/.
  • Use all_interfaces=True only on trusted networks.