Quick start¶
This guide takes you from zero to a running app in the simulator in a few minutes — even if this is your first time with tempestroid. The path is always the same: create a project, run it in the simulator, edit it and see the change live.
Prerequisites
- Python ≥ 3.11 and uv installed.
- The framework with the Qt simulator:
pip install "tempestroid[qt]"(or, in this development repository,uv sync). Details in Installation. - On WSL/Linux without a graphical environment, the Qt simulator needs a display server. See Run on device / WSL.
Step 1 — Create a project¶
You are already inside your project folder (and its virtualenv). Run
tempest new with no arguments to generate the starter files right here —
an app.py (example counter), pyproject.toml, README.md and .gitignore —
using the current folder name as the app id. No extra wrapping directory.
mkdir my-app && cd my-app # your project folder (with its venv)
uv run tempest new # scaffold HERE; id = "my-app"
Want a subdirectory? Pass a name:
uv run tempest new OtherAppcreatesOtherApp/. But the recommended flow is the in-place one above.Installed via
pip? The binary is available astempest new(withoutuv run). Throughout this guide we useuv run tempest …because it is the repository flow; drop theuv runif you installed viapip.
The generated app.py is pure Python, with no Qt import at module level —
so the same file runs in the desktop simulator, ships to the device via
tempest serve, and packages with tempest build without changing a line.
Step 2 — Run it in the simulator¶
tempest dev (with no argument) reads the app path from [tool.tempest] app in
pyproject.toml, so you run it from the project root without pointing at the
file. A window opens with the counter (-, the value, +). The terminal becomes an
interactive cockpit:
| Key | Action |
|---|---|
r |
Hot reload — reloads the code preserving the current state. |
R |
Hot restart — reloads from scratch (clean state via make_state). |
s |
Raises the simulator window to the front. |
q |
Quits. |
Pick the screen size (device presets)¶
The simulator opens at a generic phone size, but you should test at your
target devices' sizes. Pass --device (or -d) with a preset:
uv run tempest dev --device pixel-7 # 412×915 dp
uv run tempest dev -d galaxy-s24 # 384×824 dp
uv run tempest dev -d redmi-note-12 # 393×873 dp (default)
Sizes are in dp (density-independent pixels) — exactly the layout space
Compose uses on the device, so what fits in the simulator fits on the device.
Names are forgiving (pixel-7, PIXEL_7, pixel 7, Google Pixel 7 all
resolve the same). The full catalog is 33 presets (Pixel, Galaxy S/A,
Redmi/Poco/Xiaomi, Moto, OnePlus) in the Device enum — use it
programmatically with run_qt(state, view, device=Device.PIXEL_7).
Which to test
Cover a small/narrow phone (e.g. galaxy-s8, 360×740) and a large
one (e.g. pixel-8-pro, 448×998) — if the layout behaves at both extremes,
the sizes in between follow.
How faithful is the simulator?
It faithfully reflects structure, state, events and most of Style, but
not the native look (Material 3), animations, overlays and fonts — those
are only 100% faithful on the device. See
simulator fidelity.
Step 3 — Edit and see it live¶
With the simulator open, open app.py in your editor and change some text
— for example the title inside Text. Save the file. tempest dev detects
the write and hot-reloads automatically: the window updates without losing the
counter.
If an edit breaks the app, the error is printed in the terminal and the loop survives — fix it and save again. If the reload is incompatible with the live state, it falls back automatically to a clean restart.
That's the full development cycle. The rest of this guide explains what you just ran.
The mental model¶
Every tempestroid app honors a two-function contract:
make_state() -> S— returns the initial state. Called on every hot restart, so it must build clean state.Sis any object of yours (a@dataclassis the natural choice).view(app: App[S]) -> Widget— takes the app and returns the UI tree for the current state. It's a pure function of state → widgets: given the same state, it returns the same tree.
The cycle that connects the two:
state ──view(app)──▶ widget tree ──diff──▶ patches ──▶ screen
▲ │
└─────────────── app.set_state(...) ◀── event handler ◀─┘
viewbuilds the tree fromapp.state.- You wire handlers (click, etc.) to
app.set_state(...). - When a handler calls
set_state,Apprebuilds theview, diffs it against the previous tree, and hands only the patches (minimal changes) to the renderer. Multipleset_statecalls in the same tick become a single coalesced rebuild.
set_state takes a function that mutates state in place:
You never touch the screen directly — you only describe the UI as a function of state and change the state. The framework handles the rest.
A counter from scratch¶
The scaffold already gives you a full counter, but it's worth building the
minimum by hand to understand each piece. Create an app.py:
from dataclasses import dataclass
from tempestroid import App, Button, Column, Style, Text, Widget
from tempestroid.renderers.qt import run_qt
@dataclass
class CounterState:
"""The app's state: a single counter."""
value: int = 0
def make_state() -> CounterState:
"""Return the initial state (called on every hot restart)."""
return CounterState()
def view(app: App[CounterState]) -> Widget:
"""Build the UI tree for the current state."""
def increment() -> None:
app.set_state(lambda s: setattr(s, "value", s.value + 1))
return Column(
style=Style(gap=8.0),
children=[
Text(content=f"Count: {app.state.value}", key="label"),
Button(label="+", on_click=increment, key="inc"),
],
)
if __name__ == "__main__":
raise SystemExit(run_qt(make_state(), view, title="counter"))
Reading top to bottom:
CounterState— your state, a simpledataclasswith avaluefield.make_state— the initial-state factory.view— describes the screen: aColumn(stacks vertically) with aTextshowing the value and aButtonthat increments.app.state.valuereads the state;incrementcallsset_stateto change it.key="..."— identifies each widget so the diff can match the old widget to the new one across rebuilds. Give stable keys to list children.Style(gap=8.0)— spacing between children. Styles are typed, immutable objects (see the style guide).if __name__ == "__main__"—run_qtopens the window when you run the file directly. Keep the Qt import inside this block (or only here at the top, unused byview/make_state) so the file still runs on the device, which has no Qt.
Run it directly, without the cockpit:
Or with hot reload (recommended during development):
Async handlers¶
Handlers may be async — the runtime schedules them on the asyncio loop without
freezing the UI. Useful for awaiting I/O (network, disk) before updating state:
import asyncio
def view(app: App[CounterState]) -> Widget:
async def increment_later() -> None:
await asyncio.sleep(0.5)
app.set_state(lambda s: setattr(s, "value", s.value + 1))
return Button(label="+0.5s", on_click=increment_later, key="inc")
Common problems¶
| Symptom | Cause / fix |
|---|---|
ModuleNotFoundError: tempestroid |
Framework not installed in the env. Run uv sync (repo) or pip install "tempestroid[qt]". |
PySide6 / Qt import error when running dev |
The qt extra is not installed. Use pip install "tempestroid[qt]". |
app.py must define a make_state() / view |
The file must expose both functions at module level, with those exact names. |
| The window doesn't open on headless WSL/Linux | No display server. See device / WSL. |
| Edits don't reload | Make sure you're running via tempest dev (not python app.py) and that you saved the file; or press r. |
Next steps¶
- Widgets — all primitives (
Text,Column,Row,Button, inputs, media…). - Styles — the typed
Stylemodel. - Events — the typed event contract.
- CLI — all
tempestcommands. - Example gallery — full apps to study.
See also the reference example at
examples/counter/app.py.