Skip to content

Pyzed vs PyQt and PySide (Qt for Python)

Qt is the most complete GUI framework you can use from Python, through PySide6 (by The Qt Company, LGPL) or PyQt6 (by Riverbank, GPL or commercial). It has everything: charts, multimedia, an embedded Chromium, 3D, printing, accessibility, and thirty years of production use. That breadth comes with weight: a large download, a C++ object model (signals, slots, parents, layouts) and licensing to think about.

Pyzed PyQt6 / PySide6
License MIT PyQt: GPL v3 or commercial · PySide: LGPL v3 or commercial
Model State + view(); plain Python callbacks Object tree, signals and slots, or declarative QML
Look Modern components and themes, out of the box Native-like styles; modern looks through QSS or QML
Designer Pyzed Design, generates Python Qt Designer / Creator (.ui files), Qt Design Studio (QML)
Hot reload pyzed dev, keeps state QML preview in Qt tools; Widgets code needs a restart
Breadth Desktop app essentials + IDE components Almost anything, through dozens of modules
Mobile – PySide6 on Android (technical preview)
Accessibility Keyboard; no screen readers yet Full screen reader support
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)
PySide6 (Qt Widgets)
"""Reference app: a counter with -, + and Reset."""
import sys
from PySide6.QtCore import Qt
from PySide6.QtWidgets import QApplication, QHBoxLayout, QLabel, QPushButton, QVBoxLayout, QWidget
class Counter(QWidget):
def __init__(self):
super().__init__()
self.setWindowTitle("Counter")
self.resize(320, 200)
self.count = 0
self.label = QLabel("0", alignment=Qt.AlignmentFlag.AlignCenter)
self.label.setStyleSheet("font-size: 24px")
buttons = QHBoxLayout()
for text, step in (("-", -1), ("+", 1), ("Reset", None)):
button = QPushButton(text)
button.clicked.connect(lambda _=False, s=step: self.change(s))
buttons.addWidget(button)
layout = QVBoxLayout(self)
layout.addStretch()
layout.addWidget(self.label)
layout.addLayout(buttons)
layout.addStretch()
def change(self, step):
self.count = 0 if step is None else self.count + step
self.label.setText(str(self.count))
app = QApplication(sys.argv)
window = Counter()
window.show()
app.exec()

In Qt Widgets you build the widgets once and keep them in sync with your data by hand (refresh() deletes and recreates the rows). In Pyzed you only describe the rows for the current data.

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)
PySide6 (Qt Widgets)
"""Reference app: add tasks (button or Enter), tick them off, delete them, see how many are left."""
import sys
from PySide6.QtWidgets import (QApplication, QCheckBox, QHBoxLayout, QLabel, QLineEdit, QPushButton, QScrollArea,
QVBoxLayout, QWidget)
class Todo(QWidget):
def __init__(self):
super().__init__()
self.setWindowTitle("To-do")
self.resize(420, 480)
self.tasks = [] # [title, done]
self.left = QLabel()
self.left.setStyleSheet("font-size: 16px; font-weight: bold")
self.entry = QLineEdit()
self.entry.returnPressed.connect(self.add)
add = QPushButton("Add")
add.clicked.connect(self.add)
entry_row = QHBoxLayout()
entry_row.addWidget(self.entry)
entry_row.addWidget(add)
self.list = QVBoxLayout()
inner = QWidget()
column = QVBoxLayout(inner)
column.addLayout(self.list)
column.addStretch()
scroll = QScrollArea()
scroll.setWidgetResizable(True)
scroll.setWidget(inner)
layout = QVBoxLayout(self)
layout.addWidget(self.left)
layout.addLayout(entry_row)
layout.addWidget(scroll)
self.refresh()
def add(self):
if self.entry.text().strip():
self.tasks.append([self.entry.text().strip(), False])
self.entry.clear()
self.refresh()
def toggle(self, task, done):
task[1] = done
self.refresh()
def remove(self, task):
self.tasks.remove(task)
self.refresh()
def refresh(self):
while self.list.count():
item = self.list.takeAt(0)
item.widget().deleteLater()
for task in self.tasks:
row = QWidget()
line = QHBoxLayout(row)
check = QCheckBox(task[0])
check.setChecked(task[1])
check.toggled.connect(lambda done, t=task: self.toggle(t, done))
delete = QPushButton("Delete")
delete.clicked.connect(lambda _=False, t=task: self.remove(t))
line.addWidget(check, 1)
line.addWidget(delete)
self.list.addWidget(row)
self.left.setText(f"{sum(not done for _, done in self.tasks)} left")
app = QApplication(sys.argv)
window = Todo()
window.show()
app.exec()

Medians from the benchmarks (Linux, no GPU: software rendering). The install size for Qt is pyside6-essentials only; the full pyside6 package with add-ons is several times larger.

Measurement (lower is better in every row)Pyzed 1.2.0PySide6 (Qt) 6.12.0
Import the library35 ms68 ms
First frame, 10 rows165 ms135 ms
First frame, 1,000 rows243 ms217 ms
Memory (RSS), 10 rows151 MB72 MB
Memory (RSS), 1,000 rows161 MB83 MB
Update 1,000 rows18 ms24 ms
Lines: counter19 lines28 lines
Lines: to-do list31 lines62 lines
Qt Pyzed
QApplication + QMainWindow + app.exec() class MyApp(pz.App) + pz.run(MyApp)
QVBoxLayout, QHBoxLayout, QGridLayout pz.column, pz.row, pz.grid
QLabel, QPushButton, QLineEdit, QCheckBox ui.label, ui.button, ui.input, ui.checkbox
clicked.connect(slot) on_press=method
custom signals messages sent to update(), or pz.post(message) from a thread
QTimer pz.every(...) in subscriptions()
QThread / QRunnable async def callbacks, generators, or plain threads
QDockWidget, QTabWidget, QSplitter ui.workspace, ui.tab_bar, ui.split
QTreeView / QTableView + models ui.tree, ui.table, ui.virtual_list from Python lists
QSS stylesheets themes and pz.Style
QShortcut, QAction on_key(combo), commands and keys="ctrl-s" on buttons
Qt Designer .ui files Pyzed Design .pzform files with Python handlers
  1. Start from the data, not the widgets. List what your QWidget subclasses store (self.items, self.current…) and make those the attributes of your App. Everything that copies them into widgets (setText, setChecked, model resets) disappears.
  2. Turn slots into methods. A slot connected with clicked.connect(self.save) becomes on_press=self.save.
  3. Replace models with lists. A QAbstractListModel over a Python list becomes the list itself plus ui.virtual_list when it is long.
  4. Keep Qt where you depend on it. If the app needs QtWebEngine, QtMultimedia, printing or 3D, Qt is still the right tool for that app.
  • Choose Qt when you need its breadth, a screen-reader-accessible app today, or decades-proven stability across a large team, and its size and license fit your project.
  • Choose Pyzed when you want a modern-looking desktop app with far less code and ceremony, a permissive MIT license, a much smaller install, hot reload and AI assistance built into the workflow.