This tutorial mirrors the Go basics tutorial but uses the Python port of Bubble Tea. It walks you through building a small TUI program step by step.
python -m venv venv
source venv/bin/activate
pip install -e ".[dev]" # from the repo rootPython 3.10 or newer is required.
Every Bubble Tea program has three parts:
| Part | What it does |
|---|---|
| Model | Holds all program state |
| Update | Receives messages, returns new state + next command |
| View | Renders the current state as a string |
The runtime loop is:
- Call
model.init()— get the first command (if any). - Wait for a message (key press, timer, HTTP response, …).
- Call
model.update(msg)→(new_model, cmd). - Render
new_model.view()to the terminal. - Run
cmdin the background; its return value becomes the next message. - Go to step 2.
import bubblepy as tea
class Model(tea.Model):
def init(self):
return None # no initial command
def update(self, msg):
if isinstance(msg, tea.KeyMsg) and msg.key == "q":
return self, tea.quit_cmd
return self, None
def view(self):
return "Press q to quit.\n"
tea.Program(Model()).run()Save this as hello.py and run it:
python hello.pyPress q to exit.
KeyMsg.key is a string like "a", "enter", "ctrl+c", "up", "f1".
class Model(tea.Model):
def __init__(self):
self.text = ""
def init(self):
return None
def update(self, msg):
if isinstance(msg, tea.KeyMsg):
if msg.key == "enter":
return self, tea.quit_cmd
elif msg.key == "backspace":
self.text = self.text[:-1]
elif len(msg.key) == 1: # printable character
self.text += msg.key
return self, None
def view(self):
return f"Type something (Enter to quit):\n> {self.text}_\n"A command is a Callable[[], Optional[Msg]] — a function that does I/O
and returns a message. Commands always run in a background thread.
from dataclasses import dataclass
@dataclass
class TickMsg:
pass
class CountdownModel(tea.Model):
def __init__(self, n: int):
self.n = n
def init(self):
return tea.tick(1.0, TickMsg) # fire TickMsg after 1 second
def update(self, msg):
if isinstance(msg, TickMsg):
self.n -= 1
if self.n <= 0:
return self, tea.quit_cmd
return self, tea.tick(1.0, TickMsg) # re-subscribe
if isinstance(msg, tea.KeyMsg) and msg.key in ("q", "ctrl+c"):
return self, tea.quit_cmd
return self, None
def view(self):
return f"Quitting in {self.n}...\n"
tea.Program(CountdownModel(5)).run()Key points:
tea.tick(duration, MsgClass)returns a one-shot command.- To keep ticking, return
tea.tick(...)again fromupdate(). - This is the re-subscription pattern from The Elm Architecture.
Run commands in parallel and receive all results:
def init(self):
return tea.batch(
fetch_data_cmd(),
tea.tick(5.0, TimeoutMsg),
)Run commands in sequence (one after another):
return self, tea.sequence(step_one(), step_two(), step_three())Query the terminal size at startup:
def init(self):
return tea.window_size() # delivers WindowSizeMsg immediately
def update(self, msg):
if isinstance(msg, tea.WindowSizeMsg):
self.width = msg.width
self.height = msg.height
...The program also receives WindowSizeMsg automatically whenever the user
resizes the terminal window (SIGWINCH).
| Method | When to use |
|---|---|
return self, tea.quit_cmd |
Normal exit from inside the model |
p.quit() |
Quit from another thread |
p.kill() |
Immediate exit, bypass queue |
| ctrl-c / SIGINT | Delivers InterruptMsg to update(), then raises ErrInterrupted |
| ctrl-z (Unix) | SuspendMsg → suspend → ResumeMsg on resume |
Catch typed exceptions from run():
try:
p.run()
except tea.ErrInterrupted:
print("Interrupted by user")
except tea.ErrProgramKilled:
print("Killed")
except tea.ErrProgramPanic as e:
print("Crash:", e)- Commands tutorial — deep dive into Cmds.
- Browse the examples/ directory for complete programs.
- Read the CLAUDE.md for a full API reference.