guile
cheat sheet
Build lightweight desktop apps with plain Python.
installpip install guile
versionv0.6.0
requiresPython ≥ 3.9
The skeleton — every app looks like this
import guile as gui
# 1. Your existing code — untouched
def to_celsius(f):
    return round((f - 32) * 5/9, 1)
def water_phase(c):
    if c <= 0: return "Solid"
    if c < 100: return "Liquid"
    return "Gas"
1 · Existing codeOrdinary Python — guile doesn't touch it.
# 2. State
fahrenheit = gui.state(32.0)
celsius = gui.state(None)
phase = gui.state("")
2 · StateShared values. Declare at module level, outside every function.
# 3. Callback
def analyze():
    c = to_celsius(fahrenheit.value)
    celsius.set(c)
    phase.set(water_phase(c))
3 · CallbackReads .value, calls your code, writes back with .set().
# 4. Layout
@gui.app("Water", width=360, center=True)
def ui():
    gui.slider("°F", min=20, max=230,
               value=fahrenheit, on_change=fahrenheit.set)
    gui.button("Analyze", on_click=analyze)
    if celsius.value is not None:
        gui.text(f"{celsius.value} °C")
        gui.text(phase.value)
4 · LayoutRe-runs on every state change. Never call .set() inside ui().
Things to remember
Always add key= to inputs inside loops or conditionals — otherwise guile can't tell them apart after a re-render.
State lives outside ui() — declare it at module level, or it resets on every render.
Don't call .set() unconditionally inside ui() — it triggers another render and loops forever. Use on_change= callbacks instead.
Lambda captures loop variables late — lock in the current value with a default arg: lambda i=i: delete(i)
State
count  = gui.state(0)     # int
name   = gui.state("")    # str
items  = gui.state([])    # list
active = gui.state(True)  # bool

count.set(42)                   # replace
count.update(lambda x: x+1)   # transform
count.toggle()                  # bool flip

gui.text(count)              # State directly — no .value
gui.text(f"{count.value} items")
Layout — indentation = nesting
with gui.col(padding=20, gap=12):
    with gui.card():
        gui.title("Hello")
        with gui.row(gap=8, justify="flex-end"):
            gui.button("Cancel", variant="ghost")
            gui.button("Save")

# gap      space between children (px)
# padding  space inside the container
# align    cross-axis: "center", "stretch"
# justify  main axis:  "flex-end", "space-between"
Inputs
gui.button("Save", on_click=save)
gui.button("Del", variant="danger", size="sm")

name = gui.input("Name", key="name")
gui.text(f"Hello {name.value}")

temp = gui.slider("°C", min=0, max=100, key="temp")
ok   = gui.checkbox("Agree", key="ok")
unit = gui.select(["kg", "lb", "g"], "Unit", key="unit")

# bind to existing state
gui.slider("°F", value=fahrenheit,
    on_change=fahrenheit.set, key="f")
Patterns
# Conditional — plain Python
if count.value > 0:
    gui.badge("positive", variant="success")
else:
    gui.badge("zero", variant="neutral")

# Loop — key= keeps updates fast
for i, item in enumerate(items):
    with gui.row(key=f"row-{i}"):
        gui.text(item)
        gui.button("✕", on_click=lambda i=i: delete(i),
                   key=f"del-{i}")

# Toast — call from callback, never inside ui()
gui.notify("Saved!", variant="success")
Data & Plots
import pandas as pd
import matplotlib.pyplot as plt

# DataFrame, list of dicts, numpy array
gui.table(df)
gui.table(df, columns=["date", "value"])

fig, ax = plt.subplots()
ax.plot(x, y)
gui.figure(fig)
gui.figure(fig, caption="Revenue", dpi=120)
Themes & App options
gui.theme("dark")            # call first inside ui()
gui.theme("light", primary="#e11d48")
# presets: light dark neon rose forest slate

@gui.app("My App",
    width=800, height=600,
    center=True,    # center content
    resizable=True, # allow resize
    debug=True)     # open devtools
def ui(): ...
Widget quick reference
widget common options notes
gui.col() gui.row() gap padding align justify fill scroll use as with block — stack children vertically / horizontally
gui.card() gap padding margin fill raised surface; use as with block
gui.title() gui.text() size bold muted style size: xs sm md lg xl 2xl 3xl — accepts State directly
gui.badge() label variant variant: primary success danger warning neutral
gui.button() on_click variant size disabled variant: primary secondary ghost danger
gui.input() label placeholder value on_change key returns State[str] — use .value to read
gui.number_input() value min max step unit on_change returns State[float] — no string conversion needed
gui.slider() min=0 max=100 step=1 value on_change returns State[float]
gui.checkbox() label value on_change key returns State[bool]
gui.select() options label value on_change key list[str] or [(val, label), …] → State[str]
gui.tabs() labels value on_change key returns active tab as str — always add key=
gui.table() data columns max_rows=2000 DataFrame / list of dicts / 2-D list / numpy array
gui.figure() fig dpi=96 width caption any matplotlib Figure
gui.notify() message variant duration=3.0 toast — call from a callback, not inside ui()