guile
A lightweight Python framework for building desktop apps.
Philosophy
Guile started as a personal tool for building lab and research apps — the kind of quick internal dashboards, data explorers, and parameter tools that are too specific to justify a full web stack, but too interactive for a script. It does two things and tries to do them well. First, it layers interactivity on top of the Python you already have: your functions stay ordinary Python, and guile just wires them to widgets. Second, it follows a single reactive rule — change a state value and the interface updates, patching only the parts that actually changed, so text stays in inputs and focus is never lost. The app is one Python process that opens a native window: no server, no ports, no browser tab.
Install
pip install guile
Requires pywebview. On Windows, WebView2 ships with Windows 10/11 — nothing extra to install.
Quick start
import guile as gui
count = gui.state(0)
@gui.app("Counter", width=400, height=300)
def ui():
with gui.col(align="center", justify="center", style="height:100vh"):
with gui.card(gap=14):
gui.title("Counter")
with gui.row(gap=16, align="center", justify="center"):
gui.button("−", variant="secondary",
on_click=lambda: count.update(lambda x: x - 1))
gui.text(count.value, size="2xl", bold=True,
style="min-width:64px;text-align:center")
gui.button("+",
on_click=lambda: count.update(lambda x: x + 1))
gui.run()
@gui.app() defines the app; gui.run() opens the window and blocks until it is closed. Code written after gui.run() executes once the window closes — a natural place to save the session or continue a processing pipeline (see examples/field_notes.py).
While building a UI, use gui.run(dev=True): guile watches your script and reloads the app inside the open window every time you save the file. Errors show in the window without killing the session; each reload resets state to its initial values.
How it works
gui.state(value)— a reactive value; setting it re-renders the UI automatically. Read it through.value, alwayswith gui.card():/with gui.col():/with gui.row():— layout containers; everything indented goes insidegui.button(),gui.slider(),gui.input(),gui.table()— widgets that takeon_click=or return their current valuegui.figure(fig)— embed a matplotlib figure inlinegui.leaflet(center, markers=...)— embed an interactive mapgui.task(fn, on_done=...)— run slow work on a background thread; the window stays responsivegui.run()— opens the window;gui.run(dev=True)adds hot reload while you buildgui.package("my_app.py")— build a shareable executable in one call
Examples
| File | What it shows |
|---|---|
counter.py |
State, buttons, badges |
todo.py |
Lists, dynamic rendering, checkboxes |
settings.py |
Sliders, selects, form layout |
field_notes.py |
Save on exit — code after gui.run() |
mesonet_map.py |
Leaflet map with markers |
mesonet_interactive.py |
Live mesonet station data |
weather_explorer.py |
Table, date picker, file picker |
soils_lab.py |
Lab data entry form |
soil_water_retention.py |
Sliders driving a live chart |
upload_weather_data.py |
File picker, DataFrame, table |
canopeo.py |
Image analysis |
map_draw.py |
Leaflet with draw tools |
Using AI assistants with guile
If you build guile apps with an AI tool (Claude Code, Copilot, ChatGPT, …), point it at the machine-readable docs instead of the HTML pages:
- llms-full.txt — the complete API, the golden rules, and verified examples in one self-contained file. Everything an assistant needs to write correct guile apps.
- llms.txt — the short index, following the
llms.txtconvention.
Tools working inside a cloned repo can also read the guile source directly — every public function carries a full docstring.
Dependencies
| Package | Purpose |
|---|---|
pywebview |
The window (installed with guile) |
matplotlib |
gui.figure() (installed with guile) |
numpy, pandas |
Optional — pip install guile[science] |
Everything else is Python standard library.
Files
| File | Role |
|---|---|
state.py |
Reactive value class |
ui.py |
Render engine + all widgets |
_app.py |
Window lifecycle, pywebview bridge |
_template.py |
Embedded HTML/CSS/JS |
_package.py |
gui.package() — PyInstaller wrapper |
_dev.py |
gui.run(dev=True) — hot reload |
__init__.py |
Public API (gui.*) |
Changelog
v0.8.0
gui.package()now defaults topackage_mode="onedir"(a folder holding the executable and its libraries) instead of a single file. Onedir draws fewer antivirus/SmartScreen false positives and starts faster — zip it or wrap it in an installer to share. Passpackage_mode="onefile"for the old single-executable behavior. This replaces theonefile=argument.
v0.7.0
@gui.appnow only defines the app; addgui.run()at the end of your script to open the window. Code aftergui.run()executes when the window closes — a natural place to save the session or continue a pipeline (seeexamples/field_notes.py).- Hot reload:
gui.run(dev=True)watches your script and reloads the app inside the open window on every save. Errors show in the window without killing the session; each reload resets state to initial values. Stateis now fully explicit: read and compare through.value(if count.value > 0:). Comparing or operating on the State object itself raises a clearTypeError— this fixes silent misbehavior with numpy arrays and DataFrames.- New
gui.task(fn, on_done=, on_error=, busy=)— run slow work on a background thread; clicks and renders keep flowing, and state set inside the task drives live progress bars. - Errors raised in callbacks now show a danger toast in the window (previously invisible in packaged, windowed apps).
- DOM patcher fix: programmatic updates to inputs, checkboxes, textareas, and selects now render correctly after the user has interacted with them; the caret no longer jumps in
live=Trueinputs. gui.number_input()keeps.valuecurrent per keystroke but re-renders on commit (Enter, focus leave, spinner); an empty or invalid commit keeps the current value.- Maps rendered conditionally (e.g. inside a tab) now load Leaflet lazily instead of staying blank.
- Duplicate
key=values print a one-time warning;gui.progress(value, max=0)no longer raises. - New example
field_notes.py(load → run → save-on-exit); test suite expanded to 14 tests.
v0.6.0 — Improved map tile presets. Added gui.package() for one-call PyInstaller builds.
v0.5.0 — Added center=True to app window. Improved code structure in how-to page.
v0.4.0 — Added tabs. Fixed datetime-local input to display in 24-hour format.
v0.3.0 — Added notify and modal widgets.
v0.2.0 — Added max_height to gui.scroll(). Fixed multiselect change event.
v0.1.0 — First release. 27 widgets.
MIT License
Metadata
Release files for guile 0.8.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| guile-0.8.0.tar.gz | 57.1 kB | Without provenance | Details for guile-0.8.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| guile-0.8.0-py3-none-any.whl | Python 3 | none | any | Details for guile-0.8.0-py3-none-any.whl |
Total release size: 114.0 kB
Release files / guile-0.8.0.tar.gz
| Download URL | guile-0.8.0.tar.gz |
|---|---|
| Size | 57.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
e49d8bf7fba3a935920288f939db9f4fc2a22ed3208584047bfb113c1a2d86f4
|
|
BLAKE2b-256 checksum How to use checksums |
0fb37ce03396712803338b76bb32aad2fefabfbc0efe52666fed54e2e9219bf2
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.11.16
|
Release files / guile-0.8.0-py3-none-any.whl
| Download URL | guile-0.8.0-py3-none-any.whl |
|---|---|
| Size | 56.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
a0cc76a40a0875ad4a26aaaca12fd728a0f97c3ab92d473e8affeb8e917413cd
|
|
BLAKE2b-256 checksum How to use checksums |
0d1d1380ec1a6f0cb1ac08d268e05b5c9f6c7011d120cc0b6c0965dde3d9d745
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.11.16
|
Security for guile 0.8.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.