Skip to content

Key Concepts

QtPie transforms Qt development from imperative to declarative. This page explains the core building blocks you'll use in every QtPie application.


Decorators

Decorators turn regular Python classes into Qt components with minimal boilerplate.

@widget

The foundation of QtPie. Transforms a class into a Qt widget with automatic layout and initialization.

from qtpie import widget, make
from qtpy.QtWidgets import QWidget, QLabel, QPushButton

@widget(layout="vertical")
class CounterWidget(QWidget):
    label: QLabel = make(QLabel, "Count: 0")
    button: QPushButton = make(QPushButton, "Add", clicked="increment")
    count: int = 0

    def increment(self) -> None:
        self.count += 1
        self.label.setText(f"Count: {self.count}")

What it does: - Automatically creates a layout (vertical, horizontal, form, grid, or none) - Adds widget fields to the layout in declaration order - Connects signals to methods - Calls lifecycle hooks at the right time - Supports CSS-like classes for styling

Key parameters: - layout - Layout type (default: "vertical") - name - Object name for styling (default: auto-generated from class name) - classes - List of CSS classes for styling - auto_bind - Auto-bind Widget[T] fields to model properties (default: True) - undo - Enable undo/redo for model fields (default: False)

Full @widget reference →

@window

Turns a class into a QMainWindow with automatic menu bar and central widget setup.

from qtpie import window, make
from qtpy.QtWidgets import QMainWindow, QTextEdit

@window(title="Text Editor", size=(800, 600), center=True)
class EditorWindow(QMainWindow):
    editor: QTextEdit = make(QTextEdit)

    def setup(self) -> None:
        self.statusBar().showMessage("Ready")

What it does: - Sets window title, size, and icon - Auto-adds QMenu fields to the menu bar - Can center the window on screen - Supports central_widget field for main content

Full @window reference →

Creates a QMenu with automatic action and submenu population.

from qtpie import menu, action, make
from qtpy.QtWidgets import QMenu
from qtpy.QtGui import QAction

@action("&New", shortcut="Ctrl+N")
class NewAction(QAction):
    def on_triggered(self) -> None:
        print("New file!")

@menu("&File")
class FileMenu(QMenu):
    new: NewAction = make(NewAction)
    open: QAction = make(QAction, "&Open", shortcut="Ctrl+O")

What it does: - Sets menu title (auto-generated from class name if not provided) - Auto-adds QAction and QMenu fields in declaration order - Supports separator() for menu dividers

Full @menu reference →

@action

Creates a QAction with automatic signal connections.

from qtpie import action
from qtpy.QtGui import QAction

@action("&Save", shortcut="Ctrl+S", tooltip="Save the current file")
class SaveAction(QAction):
    def on_triggered(self) -> None:
        print("Saving...")

What it does: - Sets text, shortcut, tooltip, and icon - Auto-connects triggered signal to on_triggered() method - Auto-connects toggled signal to on_toggled() method (for checkable actions)

Full @action reference →

@entrypoint

Marks the application entry point. Automatically creates the QApplication and event loop when the script is run directly.

from qtpie import entrypoint, widget, make
from qtpy.QtWidgets import QWidget, QLabel

@entrypoint(dark_mode=True, title="Hello App", size=(400, 200))
@widget
class MyApp(QWidget):
    label: QLabel = make(QLabel, "Hello, QtPie!")

What it does: - Auto-runs when __name__ == "__main__" - Creates QApplication instance - Shows the window - Starts the event loop - Supports dark/light mode, window config

Full @entrypoint reference →


Factories

Factory functions create instances with declarative syntax, avoiding verbose field(default_factory=...) boilerplate.

make()

The primary factory for creating widget instances with type safety.

from qtpie import make
from qtpy.QtWidgets import QLabel, QLineEdit, QPushButton

# Basic widget creation
label: QLabel = make(QLabel, "Hello World")

# With properties
edit: QLineEdit = make(QLineEdit, placeholderText="Enter name")

# With signal connections
button: QPushButton = make(QPushButton, "Click", clicked="on_click")

# With lambda
button2: QPushButton = make(QPushButton, "Go", clicked=lambda: print("Go!"))

Key features: - Positional args passed to constructor - Keyword args can be properties (passed to constructor) or signals (connected) - form_label - Label for form layouts - grid - Position in grid layouts as (row, col) or (row, col, rowspan, colspan) - bind - Data binding path (see Data Binding section) - bind_prop - Explicit property to bind to

Full make() reference →

make_later()

Declares a field that will be initialized in setup().

from qtpie import widget, make, make_later
from qtpy.QtWidgets import QWidget, QLabel

@widget
class MyWidget(QWidget):
    label: QLabel = make_later()  # Set in setup()

    def setup(self) -> None:
        # Now we can reference other fields or self
        self.label = QLabel(f"Widget ID: {id(self)}")

Full make_later() reference →

state()

Creates reactive state fields that automatically update bound widgets when assigned.

from qtpie import widget, make, state
from qtpy.QtWidgets import QWidget, QLabel, QPushButton

@widget
class Counter(QWidget):
    count: int = state(0)  # Reactive state
    label: QLabel = make(QLabel, bind="Count: {count}")
    button: QPushButton = make(QPushButton, "+1", clicked="increment")

    def increment(self) -> None:
        self.count += 1  # Label updates automatically!

Type syntax: - Inferred from default: count: int = state(0) - Explicit type (for optionals): dog: Dog | None = state[Dog | None]() - Pre-initialized: config: Config = state(Config(debug=True))

How it works: - Backed by ObservableProxy from the Observant library - Assignment triggers change notifications - Bound widgets update automatically

Full state() reference →

spacer()

Creates flexible space in box layouts (like addStretch).

from qtpie import widget, make, spacer
from qtpy.QtWidgets import QWidget, QPushButton, QSpacerItem

@widget(layout="vertical")
class MyWidget(QWidget):
    top_button: QPushButton = make(QPushButton, "Top")
    space: QSpacerItem = spacer()  # Push everything to top
    bottom_button: QPushButton = make(QPushButton, "Bottom")

Full spacer() reference →

separator()

Creates menu separators.

from qtpie import menu, action, make, separator
from qtpy.QtWidgets import QMenu
from qtpy.QtGui import QAction

@menu
class FileMenu(QMenu):
    new: NewAction = make(NewAction)
    open: OpenAction = make(OpenAction)
    sep1: QAction = separator()  # Visual divider
    exit: ExitAction = make(ExitAction)

Full separator() reference →


Widget[T] - Model Binding

The Widget[T] base class enables automatic model binding. When you inherit from Widget[Person], QtPie automatically:

  1. Creates a model instance of type Person
  2. Creates an ObservableProxy wrapper as self.model_observable_proxy
  3. Auto-binds widget fields to model properties by matching names
from dataclasses import dataclass
from qtpie import widget, make, Widget
from qtpy.QtWidgets import QWidget, QLineEdit, QSpinBox

@dataclass
class Person:
    name: str = ""
    age: int = 0

@widget
class PersonEditor(QWidget, Widget[Person]):
    name: QLineEdit = make(QLineEdit)  # Auto-binds to model.name
    age: QSpinBox = make(QSpinBox)      # Auto-binds to model.age

What you get: - self.model - The Person instance - self.model_observable_proxy - ObservableProxy wrapping the model - Two-way data binding between widgets and model - Validation, dirty tracking, undo/redo support

Manual model creation:

@widget
class PersonEditor(QWidget, Widget[Person]):
    model: Person = make(Person, name="Alice", age=30)
    name: QLineEdit = make(QLineEdit)
    age: QSpinBox = make(QSpinBox)

Disable auto-binding:

@widget(auto_bind=False)
class PersonEditor(QWidget, Widget[Person]):
    # Now you must explicitly bind with bind="name", bind="age"
    name: QLineEdit = make(QLineEdit, bind="name")

Full Widget[T] reference → | Record Widgets guide →


Lifecycle Hook

Override setup() to customize initialization after fields are ready:

@widget
class MyWidget(QWidget):
    def setup(self) -> None:
        """Called after all fields are initialized."""
        print("Widget ready!")

Data Binding

QtPie supports powerful declarative data binding with multiple forms.

Simple state binding

Bind to a reactive state field:

@widget
class Counter(QWidget):
    count: int = state(0)
    label: QLabel = make(QLabel, bind="count")
    # When count changes, label updates

Format string binding

Embed expressions in format strings:

@widget
class Counter(QWidget):
    count: int = state(0)
    label: QLabel = make(QLabel, bind="Count: {count}")
    # Updates to "Count: 5" when count = 5

Expression binding

Use Python expressions:

@widget
class Calculator(QWidget):
    x: int = state(5)
    y: int = state(3)
    result: QLabel = make(QLabel, bind="Result: {x + y}")
    # Shows "Result: 8"

Method calls in bindings

@dataclass
class Person:
    name: str = ""

@widget
class PersonView(QWidget, Widget[Person]):
    label: QLabel = make(QLabel, bind="{name.upper()}")
    # Shows "ALICE" when name = "alice"

Format specs

@widget
class PriceDisplay(QWidget):
    price: float = state(99.99)
    label: QLabel = make(QLabel, bind="Price: ${price:.2f}")
    # Shows "Price: $99.99"

Nested paths (Widget[T] models)

@dataclass
class Address:
    city: str = ""

@dataclass
class Person:
    name: str = ""
    address: Address | None = None

@widget
class PersonEditor(QWidget, Widget[Person]):
    name: QLineEdit = make(QLineEdit)  # Auto-binds to model.name
    city: QLineEdit = make(QLineEdit, bind="address.city")  # Nested

Optional chaining

@widget
class PersonEditor(QWidget, Widget[Person]):
    # Safe even when address is None
    city: QLineEdit = make(QLineEdit, bind="address?.city")

What properties are bound? QtPie uses a binding registry to determine the default property for each widget type: - QLabeltext - QLineEdittext - QSpinBoxvalue - QCheckBoxchecked - etc.

Override with bind_prop:

label: QLabel = make(QLabel, bind="status", bind_prop="toolTip")

Format expressions guide → | Full bind() reference →


Next Steps

Now that you understand the core concepts, explore: