Skip to content

To-do list

In this tutorial we build a to-do list: the classic first app, and a good one, because it has everything a real app has: data, a form to add to it, a list to show it and ways to change it.

Goal Time Level You need
A to-do list with filters and shortcuts 30 minutes Basic · Python 5-step tutorial (steps 1 and 2)
  • Keep the app’s data in plain Python objects (a dataclass).
  • Read a text field as the user types, and submit it with Enter.
  • Draw a list of rows with a checkbox and a delete button each.
  • Filter what is shown, and add keyboard shortcuts.

The finished app

  1. The model. A task is a dataclass. The app state is the list of tasks, the text being typed and the active filter:

    @dataclass
    class Task:
    title: str
    done: bool = False
    class Todo(pz.App):
    title = "Tasks"
    size = (520, 560)
    def __init__(self):
    self.tasks = [Task("Build my first app")]
    self.draft = ""
    self.filter = "All"
  2. Adding tasks. The field is controlled: it shows self.draft and every keystroke updates it. Enter (on_submit) and the button call the same method:

    def add(self):
    if self.draft.strip():
    self.tasks.append(Task(self.draft.strip()))
    self.draft = ""
    return Command.focus("new-task") # give focus back to the field
    # in view():
    pz.row([
    ui.input(self.draft, lambda text: setattr(self, "draft", text), id="new-task",
    placeholder="What needs to be done?", on_submit=self.add, icon="plus"),
    ui.button("Add", self.add, variant="solid"),
    ], spacing=8)
  3. Drawing the list. Each task is a ui.list_item with a checkbox at the start and a delete button at the end. The lambdas capture task because task_row receives the task as an argument:

    def task_row(self, task):
    return ui.list_item(
    ui.label(task.title, color="text_muted" if task.done else "text"),
    lambda: self.toggle(task),
    start=ui.checkbox(task.done, lambda _: self.toggle(task)),
    end=ui.icon_button("trash-2", lambda: self.remove(task), size="sm", tooltip="Delete"),
    height=34,
    inset=True,
    )
  4. Filtering. ui.segmented changes self.filter, and view() only shows the visible tasks. Since view() is called again after every change, there is nothing to “refresh”:

    ui.segmented(["All", "Pending", "Done"], self.filter, lambda f: setattr(self, "filter", f))
  5. Shortcuts. on_key receives each key as text:

    def on_key(self, combo):
    if combo == "ctrl-l":
    return self.clear_done()
    if combo == "ctrl-t":
    self.theme = "One Light" if self.resolved_theme().is_dark else "One Dark"
  6. Initial focus. on_start runs once when the window opens:

    def on_start(self):
    return Command.focus("new-task")
examples/todo.py
"""A to-do list: state, text input, list items, filters and keyboard shortcuts.
python examples/todo.py
Walkthrough: https://albakore.github.io/pyzed/tutorials/basic/todo-app/
"""
from dataclasses import dataclass
from itertools import count
import pyzed as pz
from pyzed import Command, ui
_ids = count(1)
@dataclass
class Task:
title: str
done: bool = False
id: int = 0
def __post_init__(self):
self.id = self.id or next(_ids)
class Todo(pz.App):
title = "Tareas"
size = (520, 560)
min_size = (400, 360)
def __init__(self):
self.tasks = [Task("Leer la guía de Pyzed", done=True), Task("Crear mi primera app"), Task("Probar los temas")]
self.draft = ""
self.filter = "Todas"
# -- actions -----------------------------------------------------------
def add(self):
if self.draft.strip():
self.tasks.append(Task(self.draft.strip()))
self.draft = ""
return Command.focus("new-task")
def toggle(self, task):
task.done = not task.done
def remove(self, task):
self.tasks.remove(task)
def clear_done(self):
self.tasks = [t for t in self.tasks if not t.done]
def on_start(self):
return Command.focus("new-task")
def on_key(self, combo):
if combo == "ctrl-l":
return self.clear_done()
if combo == "ctrl-t":
self.theme = "One Light" if self.resolved_theme().is_dark else "One Dark"
# -- view ----------------------------------------------------------------
def visible(self):
if self.filter == "Pendientes":
return [t for t in self.tasks if not t.done]
if self.filter == "Hechas":
return [t for t in self.tasks if t.done]
return self.tasks
def task_row(self, task):
return ui.list_item(
ui.label(task.title, color="text_muted" if task.done else "text"),
lambda: self.toggle(task),
start=ui.checkbox(task.done, lambda _: self.toggle(task)),
end=ui.icon_button("trash-2", lambda: self.remove(task), size="sm", tooltip="Eliminar"),
height=34,
inset=True,
)
def view(self):
pending = sum(not t.done for t in self.tasks)
tasks = self.visible()
return pz.container(
pz.column(
[
pz.row([ui.heading("Tareas", level=2), pz.hspace(), ui.badge(f"{pending} pendientes")],
align="center"),
pz.row(
[
ui.input(self.draft, lambda text: setattr(self, "draft", text), id="new-task",
placeholder="¿Qué hay que hacer?", on_submit=self.add, icon="plus"),
ui.button("Añadir", self.add, variant="solid"),
],
spacing=8,
),
pz.row(
[
ui.segmented(["Todas", "Pendientes", "Hechas"], self.filter,
lambda f: setattr(self, "filter", f)),
pz.hspace(),
ui.button("Limpiar hechas", self.clear_done, variant="ghost", size="sm", keys="ctrl-l"),
],
align="center",
),
ui.card(
pz.scrollable(pz.column([self.task_row(t) for t in tasks], spacing=2), height="fill")
if tasks else pz.center(ui.muted("No hay tareas aquí."), height=120),
padding=6,
),
],
spacing=14,
max_width=640,
),
padding=24,
width="fill",
height="fill",
align_x="center",
bg=self.resolved_theme().editor,
)
if __name__ == "__main__":
pz.run(Todo)
Windows, macOS and Linux
uv run --with pyzed-ui python examples/todo.py
Widget Event Handler Gets
ui.input on_input lambda text: setattr(self, "draft", text) The text.
ui.input, Add on_submit, on_press self.add Nothing.
ui.segmented on_select lambda f: setattr(self, "filter", f) The filter.
ui.list_item on_press lambda: self.toggle(task) Nothing.
ui.checkbox on_toggle lambda _: self.toggle(task) True/False (ignored: _).
ui.icon_button on_press lambda: self.remove(task) Nothing.
The app on_start, on_key focus the field; ctrl-l, ctrl-t —, the combo.

Rename a task with a double click, and open a menu with a right click:

def __init__(self):
...
self.renaming = None # the task being renamed
self.menu_for = None # the task whose menu is open
def task_row(self, task):
return ui.list_item(
ui.label(task.title), lambda: self.toggle(task),
start=ui.checkbox(task.done, lambda _: self.toggle(task)),
on_double_press=lambda: setattr(self, "renaming", task), # double click
on_right_press=lambda: setattr(self, "menu_for", task), # right click
height=34, inset=True,
)
def view(self):
page = ... # the page of the tutorial
dialog = None
if self.renaming:
task = self.renaming
dialog = ui.prompt("Rename", task.title, lambda t: setattr(task, "title", t),
lambda: setattr(self, "renaming", None), lambda: setattr(self, "renaming", None))
return ui.dialog_layer(page, dialog)