Hello World [Python]
Introduction
Python is the easiest way to use Azul. You can write idiomatic Python — plain classes,
plain str, plain method calls — and the binding (uses Rusts pyo3) takes care of the rest.
Installation
Azul is not on the public PyPI (the azul package on pypi.org is an unrelated
project - do not run a plain pip install azul). There are two ways to install:
the self-hosted pip index on azul.rs, or a manual download.
Self-hosted pip index
CI publishes Linux (manylinux x86_64) wheels to a self-hosted PEP 503 index
served from azul.rs. Pointing --index-url at it makes pip resolve azul
from azul.rs instead of pypi.org:
pip install azul --index-url https://azul.rs/ui
This is NOT the public PyPI - the index is generated by the same CI run that builds the release binaries. On platforms without a wheel on that index, use the manual download below.
Manual download
Download the prebuilt extension module for your platform from the release page and put it next to your script:
# macOS
curl -L -o azul.so https://azul.rs/ui/release/0.2.0/azul.so
# linux
curl -L -o azul.so https://azul.rs/ui/release/0.2.0/azul.cpython.so
# windows
curl.exe -L -O https://azul.rs/ui/release/0.2.0/azul.pyd
The module bundles the prebuilt native library, so there are no further system
dependencies to worry about. The abi3 wheel targets Python 3.10+ (pyo3 is
abi3-py310) - make sure you have a recent version.
Note
If there is no prebuilt module for your platform, see „Building the extension“ below for the manual route.
Simple „Counter“ Example
from azul import *
# Plain Python class - "single source of truth" for app state
class DataModel:
def __init__(self, counter):
self.counter = counter
# Layout callback: f(DataModel, LayoutCallbackInfo) -> Dom. Runs once on
# startup and again after every callback that returns Update.RefreshDom.
def layout(data, info):
# Rendered counter label: a text node wrapped in a styled div.
# .with_css(...) consumes self and returns a new Dom, so builder
# calls chain inline.
label = (Dom.create_div()
.with_child(Dom.create_text(str(data.counter)))
.with_css("font-size: 32px;"))
# Button widget with a click handler. Everything lives in the flat
# `azul` module; with_on_click(data, callback) registers the handler
# and .dom() turns the widget into a Dom node.
button = (Button.create("Increase counter")
.with_on_click(data, on_click)
.dom()
.with_css("flex-grow: 1;"))
# Final wrapup - Dom.create_body builds the root, then .with_child(...)
# appends children. Builder methods return a NEW Dom - keep chaining
# (or re-assign the result); they do not mutate in place.
return (Dom.create_body()
.with_child(label)
.with_child(button))
# Click callback: f(DataModel, CallbackInfo) -> Update. 'data' is the same
# Python instance you passed to App.create, it is mutated in place (thread
# safe). Update variants are plain class attributes - no parentheses:
# return Update.RefreshDom.
def on_click(data, info):
data.counter += 1
return Update.RefreshDom
# main function
if __name__ == "__main__":
# Initialize the data model (here we set counter=5 on startup)
model = DataModel(5)
# Configure the window. layout is the "/" default route; SPA-style
# routing is done later by swapping the layout callback.
window = WindowCreateOptions.create(layout)
# AppConfig discovers system-native styling, monitor layout, etc.
# App.run blocks until the last window closes.
app = App.create(model, AppConfig.create())
app.run(window)
Three things to notice.
- Pass plain Python objects. No upcast, no downcast, no reflection macro. The binding wraps your
DataModelinstance for you and hands the same instance back to your callbacks. The framework holds a strong reference until you drop theApp, so the GC will not eat it under your feet. - Strings are
str, styles are CSS strings. NoAzString, noString(...)wrapper, noAZ_CONST_STRmacro. Pass UTF-8 Python strings; the binding converts at the boundary. - Callbacks are regular functions with the signature
(data, info) -> Update(or-> Domfor layout). Noextern "C", no boxing, no decorators — justdef.
Things we did not use that you may want to explore next.
- The
infoargument — read-only access to the system font cache, image cache, GL context, current window size, routing, and localization dictionaries inlayout; lots of mutation helpers inon_click(DOM navigation, CSS overrides without rebuilding, computed-layout queries). WindowCreateOptions— the Python binding currently exposes onlyWindowCreateOptions.create(layout); setting the window title, size, decorations etc. from Python is not wrapped yet. The underlying options are covered in windowing.
Run it
python3 hello-world.py
You should see the window pictured on the hello-world landing page. Click the button: the counter increments, the layout callback re-runs, and the new value renders.
app.run(window)opened a native window and ranlayout()once with yourDataModelon startup.- The returned
Domwas styled, laid out, and rendered. - On click, the framework matched the button's event filter, called
on_click(data, info), observed theUpdate.RefreshDomreturn, and re-invokedlayout(). - The new
Domwas diffed against the previous one; only the changed text node was repainted.
Building the extension
Only needed if there is no prebuilt module for your platform, or if you want to track master. From a checkout:
# git clone https://github.com/fschutt/azul
# cd myfolder/azul
cargo build -p azul-dll --release \
--no-default-features --features python-extension
The resulting library is target/release/libazul.{so,dylib} (azul.dll on Windows). Python imports it as azul, so rename or symlink it:
# macOS
cp target/release/libazul.dylib target/release/azul.so
# Linux
cp target/release/libazul.so target/release/azul.so
# Windows
copy target\release\azul.dll target\release\azul.pyd
Then either run Python from the directory containing the file, or prepend that path to sys.path:
import sys, os
sys.path.insert(0,
os.path.join(os.path.dirname(__file__), 'target', 'release'))
import azul
Common errors
ModuleNotFoundError: No module named 'azul'— the downloadedazul.so/azul.pydis not in the directory you are running from (or onsys.path). Runpython3from the directory containing the file, or prepend that path tosys.path.- Blank window / dead button — a callback raised an exception. The binding prints the Python traceback to stderr and falls back to a default return value (an empty
Domforlayout,DoNothingfor event callbacks), so check the terminal you started the app from. - Counter does not advance — the click callback returned
Update.DoNothing, or it implicitly returnedNone(which the binding treats asDoNothing). Always end a mutating handler withreturn Update.RefreshDom. TypeError: layout() takes 0 positional arguments but 2 were given— your callback signature is wrong.layoutand click handlers must accept exactly(data, info).- Mutation isn't sticking — you mutated a copy of the model instead of the instance bound to the framework. The binding always passes the same instance back; check that you are not shadowing
datawith a freshDataModel(...)somewhere inside the callback.
Coming Up Next
- Application Architecture — Explains the concepts of architecting a larger Azul application
- Document Object Model — The Dom tree - node types, hierarchy, and CSS
- Hello World [Rust]