Skip to content

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.

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
Pyzed
"""Reference app: a counter with -, + and Reset."""
import pyzed as pz
from 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)
Kivy (Python + KV)
"""Reference app: a counter with -, + and Reset."""
from kivy.app import App
from kivy.lang import Builder
from 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 Kivy version uses a RecycleView, Kivy’s recommended way to show lists, with its row layout in KV.

Pyzed
"""Reference app: add tasks (button or Enter), tick them off, delete them, see how many are left."""
import pyzed as pz
from 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)
Kivy (Python + KV)
"""Reference app: add tasks (button or Enter), tick them off, delete them, see how many are left."""
from kivy.app import App
from kivy.lang import Builder
from kivy.properties import BooleanProperty, ListProperty, NumericProperty, StringProperty
from 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()

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.0Kivy 2.3.1
Import the library35 ms99 ms
First frame, 10 rows165 ms258 ms
First frame, 1,000 rows243 ms1,101 ms
Memory (RSS), 10 rows151 MB146 MB
Memory (RSS), 1,000 rows161 MB282 MB
Update 1,000 rows18 ms59 ms
Lines: counter19 lines31 lines
Lines: to-do list31 lines81 lines
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
  1. Collapse KV into view(). Each KV rule becomes a function that takes data and returns widgets. Bindings such as text: str(app.count) become plain expressions: ui.heading(str(self.count)).
  2. Drop the properties. Kivy properties exist so bindings can observe changes; Pyzed re-runs view() after every event, so ordinary attributes are enough.
  3. Rethink touch-only patterns. Desktop users expect menus, keyboard shortcuts and hover states, which Pyzed components provide.
  • 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.