Pyzed vs tkinter — a modern tkinter alternative
tkinter is the GUI toolkit that comes with Python. It is
everywhere, it is small and it is fast to start, which is why most people meet it first. It also shows
its age: widgets that look like the 2000s, a layout model split between pack, grid and place, and no
answer for dark mode, hot reload or a visual designer.
CustomTkinter modernises the look on top of tkinter, and is
included in the numbers below too.
In short
Section titled “In short”| Pyzed | tkinter | |
|---|---|---|
| Install | pip install pyzed-ui |
Already there (on Linux, sometimes a python3-tk package) |
| Look | Modern ready-made components, themes, dark mode | Classic Tk, or ttk themes |
| Programming model | State + view(): the UI is rebuilt from your data |
Widgets you create, keep references to and mutate |
| Layout | Rows, columns, grids, stacks, docks, anchors | pack, grid, place |
| Drawing | GPU (wgpu), CPU fallback | Tk, on the CPU |
| Designer, hot reload | Pyzed Design, pyzed dev |
Third-party designers; no hot reload |
| Rich widgets | Code editor, command palette, file tree, tabs, toasts, markdown… | Text, Canvas, Treeview; the rest is DIY |
| Accessibility | Keyboard; no screen readers yet | Limited |
The same counter
Section titled “The same counter”Both files are the reference apps from the benchmarks, counted line by line.
"""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."""
import tkinter as tkfrom tkinter import ttk
root = tk.Tk()root.title("Counter")root.geometry("320x200")count = tk.IntVar(value=0)frame = ttk.Frame(root)frame.place(relx=0.5, rely=0.5, anchor="center")ttk.Label(frame, textvariable=count, font=("TkDefaultFont", 24)).pack(pady=8)buttons = ttk.Frame(frame)buttons.pack()ttk.Button(buttons, text="-", command=lambda: count.set(count.get() - 1)).pack(side="left", padx=4)ttk.Button(buttons, text="+", command=lambda: count.set(count.get() + 1)).pack(side="left", padx=4)ttk.Button(buttons, text="Reset", command=lambda: count.set(0)).pack(side="left", padx=4)root.mainloop()The same to-do list
Section titled “The same to-do list”Add with the button or Enter, tick tasks off, delete them, and show how many are left. In
tkinter, keeping the screen in sync with the data is your job (refresh() destroys and recreates the rows);
in Pyzed, view() always describes the current data.
"""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."""
import tkinter as tkfrom tkinter import ttk
root = tk.Tk()root.title("To-do")root.geometry("420x480")tasks = [] # [title, BooleanVar]left = ttk.Label(root, font=("TkDefaultFont", 14))left.pack(anchor="w", padx=16, pady=(16, 8))entry_row = ttk.Frame(root)entry_row.pack(fill="x", padx=16)draft = tk.StringVar()entry = ttk.Entry(entry_row, textvariable=draft)entry.pack(side="left", fill="x", expand=True)canvas = tk.Canvas(root, highlightthickness=0)bar = ttk.Scrollbar(root, command=canvas.yview)canvas.configure(yscrollcommand=bar.set)bar.pack(side="right", fill="y", pady=12)canvas.pack(fill="both", expand=True, padx=16, pady=12)inner = ttk.Frame(canvas)canvas.create_window((0, 0), window=inner, anchor="nw")inner.bind("<Configure>", lambda e: canvas.configure(scrollregion=canvas.bbox("all")))
def refresh(): for child in inner.winfo_children(): child.destroy() for task in tasks: row = ttk.Frame(inner) ttk.Checkbutton(row, text=task[0], variable=task[1], command=refresh).pack(side="left") ttk.Button(row, text="Delete", command=lambda t=task: (tasks.remove(t), refresh())).pack(side="right") row.pack(fill="x", pady=2) left.configure(text=f"{sum(not done.get() for _, done in tasks)} left")
def add(_event=None): if draft.get().strip(): tasks.append([draft.get().strip(), tk.BooleanVar(value=False)]) draft.set("") refresh()
ttk.Button(entry_row, text="Add", command=add).pack(side="left", padx=(8, 0))entry.bind("<Return>", add)refresh()root.mainloop()Measured
Section titled “Measured”Medians from the benchmarks (Linux, no GPU: software rendering, which penalises Pyzed’s GPU renderer and favours tkinter’s CPU drawing).
| Measurement (lower is better in every row) | Pyzed 1.2.0 | tkinter Tk 9.0.3 |
|---|---|---|
| Import the library | 35 ms | 5.3 ms |
| First frame, 10 rows | 165 ms | 96 ms |
| First frame, 1,000 rows | 243 ms | 157 ms |
| Memory (RSS), 10 rows | 151 MB | 25 MB |
| Memory (RSS), 1,000 rows | 161 MB | 34 MB |
| Update 1,000 rows | 18 ms | 7.7 ms |
| Lines: counter | 19 lines | 15 lines |
| Lines: to-do list | 31 lines | 39 lines |
tkinter wins on startup and size: it is a few megabytes of C that is already loaded with Python, and it draws with the CPU, which is exactly what a machine without a GPU does well. Pyzed brings a GPU renderer, a text engine and dozens of components, and still opens a 1,000-row window in a fraction of a second.
From tkinter concepts to Pyzed
Section titled “From tkinter concepts to Pyzed”| tkinter | Pyzed |
|---|---|
root = tk.Tk() … root.mainloop() |
class MyApp(pz.App) … pz.run(MyApp) |
root.title(...), root.geometry("400x300") |
class attributes title = ..., size = (400, 300) |
tk.StringVar, IntVar |
plain attributes: self.name = "" |
widget.configure(text=...) |
change the attribute; view() shows it |
ttk.Label, ttk.Button, ttk.Entry |
ui.label, ui.button, ui.input |
command=callback |
on_press=callback (or a message for update()) |
pack(side="left") / grid(row, column) |
pz.row([...]), pz.column([...]), pz.grid(...) |
Canvas + Scrollbar + inner Frame |
pz.scrollable(...) |
ttk.Treeview |
ui.tree, ui.table |
root.after(ms, fn) |
pz.every(seconds, message) in subscriptions(), or an async callback |
messagebox |
ui.confirm, ui.prompt, ui.modal |
filedialog |
No native file dialog yet: use a ui.tree of files or keep a helper library for it |
ttk.Style |
themes and styles |
Migration tips
Section titled “Migration tips”- Move state out of the widgets. Every
StringVar,IntVarand every value you read with.get()becomes an attribute of yourApp. That is most of the work. - Replace update code with
view(). Functions that destroy and rebuild frames, orconfigurelabels, disappear: writeview()once from the attributes. - Map layouts one-to-one. A
Framepacked horizontally is apz.row, vertically apz.column;gridbecomespz.grid; spacing and padding are arguments instead ofpadx/padyon every widget. - Timers and threads.
root.afterloops becomesubscriptions(); long work goes in anasync defcallback or a thread, and its result comes back as a message. - Keep tkinter where it fits. A five-line dialog in a script that must run on a bare Python is still a good tkinter use case.
When to choose which
Section titled “When to choose which”- Choose tkinter for tiny utilities and teaching material that must run with nothing installed, or when your users need screen readers on platforms where Tk supports them.
- Choose Pyzed when the app should look current, grow beyond a few windows, be designed visually, reload while you work and ship as a polished executable.