DEV Community

Cover image for I built Pyzed: native desktop GUIs in Python, drawn by Rust's Iced
Kevin Kener
Kevin Kener

Posted on AI-assisted

I built Pyzed: native desktop GUIs in Python, drawn by Rust's Iced

Every time I started a desktop tool in Python I hit the same choice. Tkinter is always there, but the result looks like 2005. Qt is powerful, but it's a big dependency with a big API. Flet and Electron-style tools look modern, but they ship a whole rendering engine next to your app.

I wanted something in between: write the app in plain Python, and get the look and speed of a modern editor like Zed. So I built Pyzed, an open-source framework where Python describes the interface and Iced, a GUI toolkit written in Rust, lays it out and draws it on the GPU.

A counter app made with Pyzed

This post covers how it works, what writing an app looks like, and a few things I learned along the way.

The 30-second version

pip install pyzed-ui
Enter fullscreen mode Exit fullscreen mode

That's one wheel with the Rust core already compiled, for Windows, macOS and Linux (x86-64 and ARM). No Rust toolchain needed, Python 3.10+. The package is pyzed-ui on PyPI and you import it as pyzed.

import pyzed as pz
from pyzed import ui


class Hello(pz.App):
    title = "Hello"
    name = ""

    def view(self):
        return pz.center(pz.column([
            ui.input(self.name, lambda text: setattr(self, "name", text), placeholder="Your name"),
            ui.heading(f"Hello, {self.name or 'world'}!"),
        ], spacing=12, max_width=320))


pz.run(Hello)
Enter fullscreen mode Exit fullscreen mode

That window already has a dark theme, focus rings, hover states and proper text rendering. There's nothing to style.

How it works

Pyzed follows the Elm architecture: your app has state, an update that changes it, and a view that turns it into widgets. Python decides what is shown; Rust decides how it's drawn.

Python                         Rust (pyzed._core, PyO3)                 Iced 0.14
App.view() ──► Widget tree ──► render.rs: Widget → iced::Element ──► layout + wgpu/tiny-skia
App.update() ◄── messages ◄────── app.rs: Message, Command → Task ◄──── events (mouse, keyboard)
Enter fullscreen mode Exit fullscreen mode
  • view() returns a tree of widgets. They're immutable Rust objects, so building the tree from Python is cheap.
  • When the user clicks or types, Pyzed calls your update(message), or the callable you passed directly (on_press=self.save), and then calls view() again.
  • Layout, text shaping, syntax highlighting and drawing all happen in Rust. Rendering goes through wgpu on the GPU, with tiny-skia as a CPU fallback.
  • The GIL is released while the event loop runs, so your Python threads keep working.

Messages can be any Python value, and update also accepts coroutines, generators and streams, so async work doesn't need special plumbing.

A slightly bigger example, a counter with buttons and icons:

import pyzed as pz
from pyzed import ui


class Counter(pz.App):
    title = "Counter"
    size = (360, 220)

    def __init__(self):
        self.count = 0

    def update(self, message):
        if message == "increment":
            self.count += 1
        elif message == "decrement":
            self.count -= 1

    def view(self):
        return pz.center(
            pz.column([
                ui.heading(str(self.count)),
                pz.row([
                    ui.button("1", "decrement", icon="minus"),
                    ui.button("1", "increment", icon="plus", variant="solid"),
                    ui.icon_button("rotate-ccw", lambda: setattr(self, "count", 0), tooltip="Reset"),
                ], spacing=8),
            ], spacing=16, align="center"),
            bg=self.theme.editor,
        )


pz.run(Counter)
Enter fullscreen mode Exit fullscreen mode

Components that look like a modern editor

Pyzed ships more than 90 components: buttons, inputs, selects, calendars, menus, toasts, a file tree, tabs, a command palette, a code editor, a terminal, charts, and virtual tables that handle hundreds of thousands of rows. They all follow the current theme, and Zed themes (One, Ayu, Gruvbox and any Zed theme file) work out of the box.

The command palette component

Around 2000 Lucide icons and the IBM Plex fonts are bundled, so icon="plus" just works.

Hot reload that keeps your state

pyzed dev main.py
Enter fullscreen mode Exit fullscreen mode

Edit view(), a helper module or a style, and save. The window updates in place with your data still there: the app object is kept and gets the new class. New methods and new attributes in __init__ show up without a restart. If the new code raises, the error shows inside the window, and the next save that fixes it brings the app back.

This was one of the features I wanted most. Restarting an app and clicking back to the screen you were working on gets old fast.

A visual designer that writes Python

pyzed design
Enter fullscreen mode Exit fullscreen mode

This opens Pyzed Design, a visual designer built with Pyzed itself. You drag components onto a form with anchors, edit their properties, double-click a control to write its event handler, and press F5 to run it. It has a code editor with completion and diagnostics (via ty and ruff), multiple cursors, folding and a minimap.

Placing controls in Pyzed Design

It's inspired by the form and scene editors of Godot, Unreal, Visual Basic and Qt Designer. It also has a node-based shader editor, in the spirit of Godot's:

Node-based shader graph

Not just forms

Since everything is drawn on the GPU, Pyzed also does animations (animate any property, transitions between windows), effects like glow, blur and glass, custom WGSL shaders with pz.shader, and small games with a game loop, held keys, sprites and particles in pyzed.game.

Neon Invaders, a game made with Pyzed

From script to executable

pyzed new my-app          # a uv project: UI in ui/, backend in src/, ruff, ty and git
cd my-app
uv run pyzed design       # open the designer on the project
uv run pyzed dev main.py  # hot reload
uv run pyzed build        # an executable for this platform
Enter fullscreen mode Exit fullscreen mode

pyzed build packages the app with PyInstaller or Nuitka, with icons and a splash screen.

Things I learned building it

Immutable widgets make the Python/Rust boundary simple. Each widget is a small Rust object created from Python and never mutated. Python can rebuild the whole tree on every change without worrying about syncing state into existing widgets, and Rust can convert it to Iced elements without locks or callbacks into Python during layout.

Release the GIL around the event loop. The event loop runs in Rust, so holding the GIL there would freeze every Python thread in the app. Releasing it, and only taking it back to call update and view, keeps background work responsive.

Headless screenshots pay for themselves. app.screenshot("ui.png") renders any app without a GPU or a display. I use it for visual regression tests and to generate every image and animation in the documentation, thousands of them. When I changed the default theme, I could regenerate the whole site instead of taking screenshots by hand.

Typed APIs and early errors matter more than speed. The package ships stubs for the Rust core, so editors complete every widget argument. Mistakes fail when the widget is created, with the value that was wrong:

ValueError: unknown button variant 'soild'
ValueError: unknown icon 'plsu'; see pyzed.icons.names()
Enter fullscreen mode Exit fullscreen mode

Where it stands today

Pyzed 1.x is stable for desktop apps on Windows, macOS and Linux, and its public API follows semantic versioning. Some honest limits:

  • It's a young project, and I'm the main maintainer.
  • It uses more memory than Tkinter, which is the price of GPU rendering and richer components.
  • Mobile and web are planned but not available yet.
  • The AI agent APIs and the animation API are still marked as provisional.

If you're choosing a GUI library, the docs have comparison pages against Tkinter, PyQt/PySide, Kivy, Flet and Dear PyGui and a benchmarks page with how each one was measured.

Try it

English · Español

Modern, fast desktop GUIs in Python, drawn natively by Iced (Rust).
Components inspired by the Zed editor, a visual designer in the spirit of Godot and Unreal node-based shaders, animations, games and built-in AI agents. A modern alternative to tkinter, PyQt, Kivy and Flet

PyPI Python CI License: MIT

📖 Documentation · ⬇️ Download Pyzed Design · 🧩 Component library · 📰 Changelog

Dragging a button, a text box and a check box from the toolbox onto a form in Pyzed Design

What is Pyzed

  • Native performance. Python describes the interface; layout, text and drawing are done by Iced 0.14 in Rust (wgpu on the GPU, tiny-skia on the CPU as a fallback). import pyzed takes about 35 ms.
  • Simple. The Elm architecture: state, update and view. A button can call a method directly (on_press=self.save), and update accepts coroutines, generators and streams.
  • More than 90 components ready to use: buttons, forms, virtual tables with hundreds of thousands of rows file tree, tabs, command palette, code editor, terminal, custom…

It's MIT licensed. I'd really like to hear what you'd build with it and what's missing for your use case, so feel free to leave a comment here or open an issue on GitHub.

I'm Kev (albakore on GitHub), the author and maintainer of Pyzed.

Top comments (0)