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 whenapp_nameis set.- Passing
app_namealone does not create files.
Hosts
Default order for mode='auto':
- PyWebView — native window
- Chrome/Chromium —
--app - Edge — Windows only if Chrome is missing
| mode | Behavior |
|---|---|
'auto' | PyWebView → Chrome → Edge (Windows) |
'webview' | PyWebView only |
'chrome' / 'edge' | Force that browser |
None | Server 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
- Clean virtualenv
pip install "glue-ui[build]"python -m glue your_script.py ui- Ship from
dist/; add--onefile --noconsolewhen 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=Trueonly on trusted networks.