Pyzed vs Kivy for Python desktop apps
Kivy and Pyzed have the same basic idea: draw every widget yourself on the GPU instead of asking the operating system for native ones. Kivy was built for touch screens and runs on Android and iOS, which Pyzed does not. Pyzed is built for desktop productivity apps and comes with the components, themes and tooling for them.
In short
Section titled “In short”| Pyzed | Kivy | |
|---|---|---|
| Platforms | Windows, macOS, Linux | Windows, macOS, Linux, Android, iOS |
| Drawing | wgpu (Vulkan, Metal, DX12, GL), CPU fallback | OpenGL ES 2 |
| Look | Modern desktop components, themes, dark mode | Kivy’s own minimal style; Material with KivyMD |
| UI description | Python only | Python plus the KV language |
| State | Plain attributes, view() rebuilt on change |
Kivy properties and bindings |
| Desktop details | Menus, shortcuts, tabs, trees, docks, code editor, multi-window | Touch-first; desktop conventions are up to you |
| Designer, hot reload | Pyzed Design, pyzed dev |
Third-party tools |
| Accessibility | Keyboard; no screen readers yet | No screen readers |
The same counter
Section titled “The same counter”"""Reference app: a counter with -, + and Reset."""
import pyzed as pzfrom pyzed import ui
class Counter(pz.App): title = "Counter" size = (320, 200)
def __init__(self): self.count = 0
def add(self, step): self.count += step
def view(self): return pz.center(pz.column([ ui.heading(str(self.count)), pz.row([ ui.button(icon="minus", on_press=lambda: self.add(-1)), ui.button(icon="plus", on_press=lambda: self.add(1), variant="solid"), ui.button("Reset", lambda: setattr(self, "count", 0), variant="ghost"), ], spacing=8), ], spacing=16, align="center"))
pz.run(Counter)"""Reference app: a counter with -, + and Reset."""
from kivy.app import Appfrom kivy.lang import Builderfrom kivy.properties import NumericProperty
KV = """AnchorLayout: BoxLayout: orientation: "vertical" size_hint: None, None size: 240, 120 spacing: 8 Label: text: str(app.count) font_size: 32 BoxLayout: spacing: 8 Button: text: "-" on_press: app.count -= 1 Button: text: "+" on_press: app.count += 1 Button: text: "Reset" on_press: app.count = 0"""
class CounterApp(App): count = NumericProperty(0) title = "Counter"
def build(self): return Builder.load_string(KV)
CounterApp().run()The same to-do list
Section titled “The same to-do list”The Kivy version uses a RecycleView, Kivy’s recommended way to show lists, with its row layout in KV.
"""Reference app: add tasks (button or Enter), tick them off, delete them, see how many are left."""
import pyzed as pzfrom pyzed import ui
class Todo(pz.App): title = "To-do" size = (420, 480)
def __init__(self): self.tasks = [] # [title, done] self.draft = ""
def add(self): if self.draft.strip(): self.tasks.append([self.draft.strip(), False]) self.draft = ""
def toggle(self, task, done): task[1] = done
def row(self, task): return pz.row([ ui.checkbox(task[1], lambda done: self.toggle(task, done)), ui.label(task[0], color="text_muted" if task[1] else "text", width="fill"), ui.icon_button("trash-2", lambda: self.tasks.remove(task)), ], spacing=8, align="center")
def view(self): left = sum(not done for _, done in self.tasks) return pz.column([ ui.heading(f"{left} left", level=3), pz.row([ ui.input(self.draft, lambda text: setattr(self, "draft", text), on_submit=self.add), ui.button("Add", self.add, variant="solid"), ], spacing=8), pz.scrollable(pz.column([self.row(t) for t in self.tasks], spacing=4), height="fill"), ], spacing=12, padding=16)
pz.run(Todo)"""Reference app: add tasks (button or Enter), tick them off, delete them, see how many are left."""
from kivy.app import Appfrom kivy.lang import Builderfrom kivy.properties import BooleanProperty, ListProperty, NumericProperty, StringPropertyfrom kivy.uix.boxlayout import BoxLayout
KV = """<TaskRow>: size_hint_y: None height: 40 CheckBox: size_hint_x: None width: 40 active: root.done on_active: app.toggle(root.index, self.active) Label: text: root.title color: (.5, .5, .5, 1) if root.done else (1, 1, 1, 1) text_size: self.size valign: "middle" Button: text: "Delete" size_hint_x: None width: 80 on_press: app.remove(root.index)
BoxLayout: orientation: "vertical" padding: 16 spacing: 12 Label: text: f"{app.left} left" size_hint_y: None height: 32 font_size: 20 BoxLayout: size_hint_y: None height: 40 spacing: 8 TextInput: id: entry multiline: False on_text_validate: app.add(self) Button: text: "Add" size_hint_x: None width: 80 on_press: app.add(entry) RecycleView: data: app.rows viewclass: "TaskRow" RecycleBoxLayout: orientation: "vertical" size_hint_y: None height: self.minimum_height default_size_hint: 1, None default_size: None, 40"""
class TaskRow(BoxLayout): index = NumericProperty(0) title = StringProperty("") done = BooleanProperty(False)
class TodoApp(App): title = "To-do" rows = ListProperty([]) left = NumericProperty(0)
def build(self): return Builder.load_string(KV)
def refresh(self): self.rows = [{**row, "index": i} for i, row in enumerate(self.rows)] self.left = sum(not row["done"] for row in self.rows)
def add(self, entry): if entry.text.strip(): self.rows.append({"title": entry.text.strip(), "done": False}) entry.text = "" self.refresh()
def toggle(self, index, done): self.rows[index]["done"] = done self.refresh()
def remove(self, index): del self.rows[index] self.refresh()
TodoApp().run()Measured
Section titled “Measured”Medians from the benchmarks (Linux, no GPU: both toolkits render through Mesa’s software OpenGL/Vulkan, so the comparison between these two is like for like).
| Measurement (lower is better in every row) | Pyzed 1.2.0 | Kivy 2.3.1 |
|---|---|---|
| Import the library | 35 ms | 99 ms |
| First frame, 10 rows | 165 ms | 258 ms |
| First frame, 1,000 rows | 243 ms | 1,101 ms |
| Memory (RSS), 10 rows | 151 MB | 146 MB |
| Memory (RSS), 1,000 rows | 161 MB | 282 MB |
| Update 1,000 rows | 18 ms | 59 ms |
| Lines: counter | 19 lines | 31 lines |
| Lines: to-do list | 31 lines | 81 lines |
From Kivy concepts to Pyzed
Section titled “From Kivy concepts to Pyzed”| Kivy | Pyzed |
|---|---|
App.build() returns the root widget |
App.view() returns the tree, every time state changes |
NumericProperty, StringProperty, ListProperty |
plain attributes |
KV rules (<MyRow>:) |
Python functions that return widgets, or components |
BoxLayout(orientation=...) |
pz.row / pz.column |
GridLayout, AnchorLayout, FloatLayout |
pz.grid, pz.center / alignment, pz.stack + pz.anchor_layout |
ScrollView, RecycleView |
pz.scrollable, ui.virtual_list |
on_press: app.do() |
on_press=self.do |
Clock.schedule_interval |
pz.every(...) in subscriptions() |
Animation(...) |
pz.Animation and transitions |
Window.clearcolor, KivyMD themes |
themes |
Migration tips
Section titled “Migration tips”- Collapse KV into
view(). Each KV rule becomes a function that takes data and returns widgets. Bindings such astext: str(app.count)become plain expressions:ui.heading(str(self.count)). - Drop the properties. Kivy properties exist so bindings can observe changes; Pyzed re-runs
view()after every event, so ordinary attributes are enough. - Rethink touch-only patterns. Desktop users expect menus, keyboard shortcuts and hover states, which Pyzed components provide.
When to choose which
Section titled “When to choose which”- Choose Kivy when the same app must run on phones and tablets, or for touch kiosks and multimedia apps.
- Choose Pyzed for desktop apps that should look and behave like modern desktop software, with a designer, hot reload and AI tooling, and no second language to learn.