Skip to content

Pyzed vs Flet for Python desktop apps

Flet lets you build Flutter apps in Python. Your Python code runs in one process and sends the control tree to a Flutter client that draws it, on the desktop, on the web or on a phone. That reach is Flet’s big strength. Pyzed draws in the same process as your Python code, through a Rust renderer, and focuses on the desktop.

Pyzed Flet
Architecture One process: Python + Rust renderer (Iced) Python process + Flutter client process, talking over a protocol
Platforms Windows, macOS, Linux Windows, macOS, Linux, web, Android, iOS
Look Modern code-editor style, dense, desktop-first Material 3 or Cupertino, mobile-first
Model State + view() rebuilt from data Mutable controls + page.update(); Flet 1.0 adds declarative components with hooks
Packaging pyzed build (PyInstaller), no extra SDK flet build (needs the Flutter SDK)
Designer Pyzed Design –
Hot reload Keeps app state flet run restarts the app on save
Accessibility Keyboard; no screen readers yet Screen readers through Flutter’s semantics
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)
Flet
"""Reference app: a counter with -, + and Reset."""
import flet as ft
def main(page: ft.Page):
page.title = "Counter"
page.window.width, page.window.height = 320, 200
count = ft.Text("0", size=24)
def change(step):
count.value = "0" if step is None else str(int(count.value) + step)
page.update()
page.add(ft.Column([
count,
ft.Row([
ft.IconButton(ft.Icons.REMOVE, on_click=lambda e: change(-1)),
ft.IconButton(ft.Icons.ADD, on_click=lambda e: change(1)),
ft.TextButton("Reset", on_click=lambda e: change(None)),
], alignment=ft.MainAxisAlignment.CENTER),
], horizontal_alignment=ft.CrossAxisAlignment.CENTER))
ft.run(main)
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)
Flet
"""Reference app: add tasks (button or Enter), tick them off, delete them, see how many are left."""
import flet as ft
def main(page: ft.Page):
page.title = "To-do"
page.window.width, page.window.height = 420, 480
tasks = ft.Column(scroll=ft.ScrollMode.AUTO, expand=True)
left = ft.Text("0 left", size=18, weight=ft.FontWeight.BOLD)
def refresh(_=None):
left.value = f"{sum(not row.controls[0].value for row in tasks.controls)} left"
page.update()
def remove(row):
tasks.controls.remove(row)
refresh()
def add(_):
if entry.value.strip():
row = ft.Row([ft.Checkbox(label=entry.value.strip(), on_change=refresh, expand=True)])
row.controls.append(ft.IconButton(ft.Icons.DELETE, on_click=lambda e: remove(row)))
tasks.controls.append(row)
entry.value = ""
refresh()
entry = ft.TextField(expand=True, on_submit=add)
page.add(left, ft.Row([entry, ft.Button("Add", on_click=add)]), tasks)
ft.run(main)

Medians from the benchmarks (Linux, no GPU: software rendering for both). Flet’s memory includes its Flutter client process, which is where the window lives. Its install size counts the Python packages plus the desktop client that flet downloads on first run.

Measurement (lower is better in every row)Pyzed 1.2.0Flet 1.0.4
Import the library35 ms40 ms
First frame, 10 rows165 ms579 ms
First frame, 1,000 rows243 ms994 ms
Memory (RSS), 10 rows151 MB268 MB
Memory (RSS), 1,000 rows161 MB395 MB
Update 1,000 rows18 ms166 ms
Lines: counter19 lines17 lines
Lines: to-do list31 lines22 lines
Flet Pyzed
ft.run(main) with main(page) pz.run(MyApp) with a pz.App class
page.add(...), page.update() return the tree from view(); no manual update
ft.Column, ft.Row, ft.Container pz.column, pz.row, pz.container
ft.Text, ft.Button, ft.TextField, ft.Checkbox ui.label, ui.button, ui.input, ui.checkbox
on_click=handler(e) on_press=method (no event argument needed)
page.theme_mode = ft.ThemeMode.DARK theme = "One Dark" or pz.themes.auto(...)
page.run_task(coro) return a coroutine from a callback
ft.ListView ui.virtual_list
ft.NavigationRail, ft.Tabs ui.workspace panels, ui.tab_bar
  1. Move control state into the app. Values you read back from controls (field.value, checkbox.value) become attributes, set by the controls’ callbacks.
  2. Delete page.update() calls. Pyzed redraws after every callback.
  3. Expect a denser UI. Material spacing is made for touch; Pyzed’s components are sized like a desktop editor. Use size="lg" variants or styles if you want more air.
  • Choose Flet when one codebase must run on the web and on phones, or you want Flutter’s Material widgets.
  • Choose Pyzed for desktop-first apps: one process, no Flutter SDK to package, a visual designer, hot reload that keeps your state, and a dense, professional look.