Hello World - Your First QtPie App¶
Let's build a Qt app, step by step, from the simplest possible widget to a reactive counter.
Step 1: Just a Label¶
The simplest QtPie app is a function that returns a widget:
from qtpie import entrypoint
from qtpy.QtWidgets import QLabel
@entrypoint
def main():
return QLabel("Hello, World!")
Save this as hello.py and run it:
That's it. A window appears with "Hello, World!". No QApplication, no if __name__ == "__main__", no boilerplate.
| Light mode | Dark mode |
|---|---|
![]() |
![]() |
What @entrypoint does:
- Creates a QApplication instance
- Calls your function to get a widget
- Shows the widget in a window
- Runs the event loop
Step 2: A Widget Class with a Button¶
Let's make a real widget with multiple children:
from qtpie import entrypoint, make, widget
from qtpy.QtWidgets import QLabel, QPushButton, QWidget
@entrypoint
@widget
class MyWidget(QWidget):
text: QLabel = make(QLabel, "Hello, World!")
button: QPushButton = make(QPushButton, "Click Me", clicked="on_click")
def on_click(self):
self.text.setText("Button Clicked!")
Run it, click the button. The label updates.
What @widget does:
- Calls super().__init__() for you
- Creates a vertical layout automatically
- Adds text and button to the layout in declaration order
- Sets the widget's objectName to "MyWidget"
What make() does:
- Creates widget instances: make(QLabel, "Hello") → QLabel("Hello")
- Connects signals: clicked="on_click" → button.clicked.connect(self.on_click)
- The field name becomes the widget's objectName (useful for styling)
No More Boilerplate¶
Compare to plain PySide6:
# Plain PySide6 - lots of ceremony
class MyWidget(QWidget):
def __init__(self):
super().__init__()
self.setObjectName("MyWidget")
layout = QVBoxLayout(self)
self.text = QLabel("Hello, World!")
self.text.setObjectName("text")
layout.addWidget(self.text)
self.button = QPushButton("Click Me")
self.button.setObjectName("button")
self.button.clicked.connect(self.on_click)
layout.addWidget(self.button)
def on_click(self):
self.text.setText("Button Clicked!")
if __name__ == "__main__":
app = QApplication([])
window = MyWidget()
window.show()
app.exec()
35 lines of boilerplate vs 12 lines of intent.
Step 3: Add State¶
Right now, clicking the button just sets static text. Let's add a counter:
from qtpie import entrypoint, make, widget
from qtpy.QtWidgets import QLabel, QPushButton, QWidget
@entrypoint
@widget
class Counter(QWidget):
label: QLabel = make(QLabel, "Count: 0")
button: QPushButton = make(QPushButton, "Add", clicked="increment")
count: int = 0
def increment(self):
self.count += 1
self.label.setText(f"Count: {self.count}")
This works, but notice we're still manually updating the label with setText(). We can do better.
Step 4: Reactive State with state()¶
Here's where QtPie shines. Use state() to create reactive state, and bind= to connect widgets:
from qtpie import entrypoint, make, state, widget
from qtpy.QtWidgets import QLabel, QPushButton, QWidget
@entrypoint
@widget
class Counter(QWidget):
count: int = state(0)
label: QLabel = make(QLabel, bind="Count: {count}")
button: QPushButton = make(QPushButton, "Add", clicked="increment")
def increment(self):
self.count += 1
That's it. No manual setText(). When self.count changes, the label updates automatically.
What state() does:
- Creates a reactive field: count: int = state(0) starts at 0
- When you assign self.count = 42, it notifies all bound widgets
- Widgets bound with bind="count" update automatically
What bind= does:
- bind="count" → binds to self.count
- bind="Count: {count}" → format string, updates to "Count: 0", "Count: 1", etc.
- bind="{count * 2}" → expressions work too: 0, 2, 4, 6...
Two-Way Binding¶
Binding works both ways for input widgets:
from qtpie import entrypoint, make, state, widget
from qtpy.QtWidgets import QLabel, QLineEdit, QWidget
@entrypoint
@widget
class Greeter(QWidget):
name: str = state("")
name_input: QLineEdit = make(QLineEdit, bind="name")
greeting: QLabel = make(QLabel, bind="Hello, {name}!")
- Type in the input →
self.nameupdates → label updates - Change
self.namein code → input updates
Advanced: Multiple Variables in Bindings¶
from qtpie import entrypoint, make, state, widget
from qtpy.QtWidgets import QLabel, QSpinBox, QWidget
@entrypoint
@widget
class Calculator(QWidget):
a: int = state(10)
b: int = state(20)
spin_a: QSpinBox = make(QSpinBox, bind="a")
spin_b: QSpinBox = make(QSpinBox, bind="b")
result: QLabel = make(QLabel, bind="{a} + {b} = {a + b}")
Change either spinbox → both state fields update → result recalculates automatically.
Complete Example: The Counter¶
Here's the canonical QtPie example - the "Hello World" of reactive frameworks:
from qtpie import entrypoint, make, state, widget
from qtpy.QtWidgets import QLabel, QPushButton, QWidget
@entrypoint
@widget
class Counter(QWidget):
count: int = state(0)
label: QLabel = make(QLabel, bind="Count: {count}")
button: QPushButton = make(QPushButton, "Add", clicked="increment")
def increment(self):
self.count += 1
12 lines. Zero boilerplate. Fully reactive.
Compare to plain PySide6:
from qtpy.QtWidgets import QApplication, QLabel, QPushButton, QVBoxLayout, QWidget
class Counter(QWidget):
def __init__(self):
super().__init__()
self.setObjectName("Counter")
self.count = 0
layout = QVBoxLayout(self)
self.label = QLabel("Count: 0")
self.label.setObjectName("label")
layout.addWidget(self.label)
self.button = QPushButton("Add")
self.button.setObjectName("button")
self.button.clicked.connect(self.increment)
layout.addWidget(self.button)
def increment(self):
self.count += 1
self.label.setText(f"Count: {self.count}")
if __name__ == "__main__":
app = QApplication([])
window = Counter()
window.show()
app.exec()
35 lines vs 12 lines. Same functionality.
What's Next?¶
Now that you've built your first QtPie app, explore:
- Key Concepts - Deeper dive into
@widget,make(), andstate() - Layouts - Horizontal, form, and grid layouts
- Signals - More ways to connect signals
- Reactive State - Full power of
state()and binding - Styling - CSS classes and themes
Or jump straight to the Examples for real-world patterns.

