pyside — Cursor rule: what it does, how to install
Enforces clean PySide6 desktop patterns in Cursor. Keeps UI separate from application logic via Qt Designer or QML with Python backends.
.cursor/rules/pyside.mdcWhat pyside does
This rule guides Cursor to build PySide6 applications with clean architecture. It prevents the assistant from writing UI layouts by hand in Python. Instead, it mandates designing interfaces in Qt Designer, compiling .ui files with pyside6-uic, and importing the resulting classes into dedicated controller classes. Manually editing generated ui_*.py files is explicitly disallowed.
When working with dynamic Qt Quick interfaces, the rule keeps QML purely declarative. All heavy computations and state handling live in Python subclasses of QObject, exposed via @Property, Signal, and @Slot. Heavy logic directly inside QML is marked as bad practice.
What it helps with
- Separate PySide6 interface layout from application logic
- Integrate
pyside6-uicoutput into controller classes cleanly - Offload calculations from QML components into Python
QObjectslots - Expose reactive state to Qt Quick using
@PropertyandSignal
Example prompts for the agent
- Write a Python controller window that loads the generated
ui_main.pyclass - Create a
QObjectbackend to supply values to my QML text fields - Refactor this QML button handler to call a Python
@Slot
How to install pyside
Manually: put the files in place
.cursor/rules/pyside.mdcFrontmatter: description, globs, alwaysApply
Manually: put the files in place
.claude/rules/pyside.mdThe paths frontmatter field limits the files: move globs from the source into paths
AGENTS.md
Codex reads AGENTS.md: paste the rule text there
Manually: put the files in place
.github/instructions/pyside.instructions.mdThe applyTo field sets which files it applies to: move globs from the source into applyTo
Manually: put the files in place
.devin/rules/pyside.mdManually: put the files in place
.clinerules/pyside.mdStep by step, with commands for each platform: Cursor rules: .mdc files, four modes and what replaced .cursorrules
Questions about pyside
How do I install the pyside rule in Cursor?
Place the rule configuration file into .cursor/rules/pyside.mdc inside your project root.
What does the pyside rule do?
It instructs the AI assistant to separate visual layout from business logic in PySide6, relying on visual Qt Designer files or clean QML bindings.
Can generated ui files be modified directly under this rule?
The rule forbids manual edits to ui_*.py files. All signal wiring and event handling must reside in separate controller classes.
Source text
pyside.mdc · CC0-1.0
---
description: Definitive guide for building modern, maintainable, and performant PySide6 applications using best practices like UI/logic separation, modern controls, and robust type safety.
globs: **/*.py
---
# PySide6 Best Practices
This guide outlines the essential best practices for developing robust, maintainable, and modern PySide6 applications. Adhere to these principles to ensure high-quality, performant, and future-proof code.
## 1. Code Organization & UI Generation
**Principle:** Strictly separate UI definition from application logic. Leverage Qt Designer for visual UI creation and `pyside6-uic` for generating Python UI classes.
**Rule:** Always design your user interfaces visually in Qt Designer. Convert the `.ui` files to Python classes using `pyside6-uic`, then import and compose these generated UI classes within a dedicated Python controller class. **Never manually modify the generated `ui_*.py` files.**
❌ **BAD:** Hand-coding complex UI layouts directly in Python, or modifying generated UI files.
```python
# main.py (Bad: Hand-coding UI directly)
from PySide6.QtWidgets import QApplication, QMainWindow, QPushButton, QVBoxLayout, QWidget
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Bad UI Design - Hand-coded")
central_widget = QWidget()
self.setCentralWidget(central_widget)
layout = QVBoxLayout(central_widget)
self.button = QPushButton("Click Me")
layout.addWidget(self.button)
self.button.clicked.connect(self.on_button_clicked)
def on_button_clicked(self):
print("Button clicked!")
```
✅ **GOOD:** Use `pyside6-uic` generated UI classes composed in a controller.
```python
# 1. Design 'my_app.ui' in Qt Designer (e.g., a QMainWindow with a QPushButton named 'myButton').
# 2. Run: pyside6-uic my_app.ui -o ui_my_app.py
#
# ui_my_app.py (Generated file - DO NOT MODIFY MANUALLY)
# from PySide6 import QtCore, QtWidgets
# class Ui_MainWindow(object):
# def setupUi(self, MainWindow):
# MainWindow.setObjectName("MainWindow")
# self.centralwidget = QtWidgets.QWidget(MainWindow)
# self.myButton = QtWidgets.QPushButton(self.centralwidget)
# self.myButton.setObjectName("myButton")
# MainWindow.setCentralWidget(self.centralwidget)
# self.retranslateUi(MainWindow)
# QtCore.QMetaObject.connectSlotsByName(MainWindow)
# def retranslateUi(self, MainWindow):
# _translate = QtCore.QCoreApplication.translate
# MainWindow.setWindowTitle(_translate("MainWindow", "My App"))
# self.myButton.setText(_translate("MainWindow", "Click Me"))
# main.py (Controller class)
from PySide6.QtWidgets import QApplication, QMainWindow
from ui_my_app import Ui_MainWindow # Import the generated UI class
import sys
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.ui = Ui_MainWindow()
self.ui.setupUi(self) # Initialize the UI from the generated class
self.setWindowTitle("Good UI Design - Composed") # Override title if needed
# Connect signals AFTER setupUi
self.ui.myButton.clicked.connect(self._on_button_clicked)
def _on_button_clicked(self):
print("Button clicked from composed UI!")
if __name__ == "__main__":
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())
```
## 2. Modern Controls & QML
**Principle:** Embrace modern UI/UX with Qt Quick Controls 2 for fluid, material-style components. Use QML for declarative UI, and Python for business logic.
**Rule:** Prefer Qt Quick Controls 2 for new UIs. Only create custom QML controls when built-in options are insufficient. Keep QML focused on UI declaration; expose data and complex logic from Python via `QObject` properties and slots.
❌ **BAD:** Mixing complex business logic directly into QML, or using QWidgets for highly dynamic/animated interfaces where QML excels.
```qml
// main.qml (Bad: Complex logic in QML)
import QtQuick
import QtQuick.Controls
ApplicationWindow {
width: 640; height: 480; visible: true
title: "Bad QML - Logic in UI"
TextField { id: inputField; text: "10" }
Button {
text: "Calculate Factorial"
onClicked: {
// Bad: Complex calculation directly in QML
let n = parseInt(inputField.text);
let result = 1;
for (let i = 2; i <= n; i++) {
result *= i;
}
resultLabel.text = "Factorial: " + result;
}
}
Label { id: resultLabel; text: "Result: " }
}
```
✅ **GOOD:** QML for UI, Python for logic.
```python
# backend.py (Python backend for QML)
from PySide6.QtCore import QObject, Property, Signal, Slot
import math
class Backend(QObject):
def __init__(self, parent=None):
super().__init__(parent)
self._input_value = "10"
self._result_value = ""
inputValueChanged = Signal()
resultValueChanged = Signal()
@Property(str, notify=inputValueChanged)
def inputValue(self) -> str:
return self._input_value
@inputValue.setter
def inputValue(self, value: str):
if self._input_value != value:
self._input_value = value
self.inputValueChanged.emit()
@Property(str, notify=resultValueChanged)
def resultValue(self) -> str:
return self._result_value
@resultValue.setter
def resultValue(self, value: str):
if self._result_value != value:
self._result_value = value
self.resultValueChanged.emit()
@Slot()
def calculateFactorial(self) -> None:
try:
n = int(self.inputValue)
if n < 0:
self.resultValue = "Error: Negative input"
else:
self.resultValue = f"Factorial: {math.factorial(n)}"
except ValueError:
self.resultValue = "Error: Invalid number"
# main.py (QML Application entry point)
from PySide6.QtGui import QGuiApplication
from PySide6.QtQml import QQmlApplicationEngine
import sys
from backend import Backend
if __name__ == "__main__":
app = QGuiApplication(sys.argv)
engine = QQmlApplicationEngine()
backend = Backend()
engine.rootContext().setContextProperty("backend", backend) # Expose Python object to QML
engine.load("main.qml")
if not engine.rootObjects():
sys.exit(-1)
sys.exit(app.exec())
# main.qml (QML UI - uses backend)
import QtQuick
import QtQuick.Controls
ApplicationWindow {
width: 640; height: 480; visible: true
title: "Good QML - UI Only"
ColumnLayout {
anchors.fill: parent
anchors.margins: 20
TextField {
Layout.fillWidth: true
placeholderText: "Enter a number"
text: backend.inputValue // Bind to Python property
onTextChanged: backend.inputValue = text
}
Button {
Layout.fillWidth: true
text: "Calculate Factorial"
onClicked: backend.calculateFactorial() // Call Python slot
}
Label {
Layout.fillWidth: true
text: backend.resultValue // Bind to Python property
}
}
}
```
## 3. Type Safety & Linting
**Principle:** Leverage PySide6's robust type hints for early error detection and improved code readability.
**Rule:** Always use type hints for all PySide6 properties, signals, slots, and method parameters. Enforce PEP 8 naming conventions (snake_case for Python functions/variables, CamelCase for Qt classes/methods) using `black` for formatting and `mypy`/`pylint` for static analysis in CI.
❌ **BAD:** Untyped code, inconsistent naming.
```python
# Bad: No type hints, inconsistent naming
class MyWidget(QMainWindow):
def __init__(self):
super().__init__()
self.my_button = QPushButton("Click")
self.my_button.clicked.connect(self.handle_click)
def handle_click(self): # Should be _handle_click or handle_click_event
print("Clicked")
def AnotherFunction(arg): # Should be another_function
pass
```
✅ **GOOD:** Fully typed, PEP 8 compliant, clear intent.
```python
# Good: Type hints, PEP 8 compliant
from PySide6.QtWidgets import QApplication, QMainWindow, QPushButton, QWidget
from PySide6.QtCore import Slot
class MyWidget(QMainWindow):
def __init__(self, parent: QWidget | None = None) -> None:
super().__init__(parent)
self.my_button: QPushButton = QPushButton("Click Me")
self.setCentralWidget(self.my_button)
self.my_button.clicked.connect(self._handle_click) # Private method convention
@Slot()
def _handle_click(self) -> None:
"""Handles the button click event."""
print("Button was clicked!")
def another_helper_function(data: str) -> bool:
"""A helper function with type hints."""
return len(data) > 0
```
## 4. Styling & Theming
**Principle:** Achieve a consistent, modern UI aesthetic across your application.
**Rule:** Use `QtVSCodeStyle` for applying modern, VS Code-inspired themes. Always set the `Qt.ApplicationAttribute.AA_UseHighDpiPixmaps` attribute for crisp SVG icons on high-DPI displays.
❌ **BAD:** Default Qt styling, blurry icons on high-DPI screens.
```python
# Bad: Default Qt style, no high-DPI fix
import sys
from PySide6.QtWidgets import QApplication, QMainWindow, QPushButton
app = QApplication(sys.argv)
# No stylesheet, no high-DPI attribute
main_win = QMainWindow()
push_button = QPushButton("Unstyled Button")
main_win.setCentralWidget(push_button)
main_win.show()
sys.exit(app.exec())
```
✅ **GOOD:** Consistent theme, high-DPI support.
```python
# Good: QtVSCodeStyle, high-DPI fix
import sys
from PySide6.QtWidgets import QApplication, QMainWindow, QPushButton
from PySide6.QtCore import Qt
import qtvscodestyle as qtvsc
app = QApplication(sys.argv)
app.setAttribute(Qt.ApplicationAttribute.AA_UseHighDpiPixmaps) # Essential for crisp SVGs
# Load a dark VS Code theme
stylesheet = qtvsc.load_stylesheet(qtvsc.Theme.DARK_VS)
app.setStyleSheet(stylesheet)
main_win = QMainWindow()
push_button = QPushButton("Styled Button")
main_win.setCentralWidget(push_button)
main_win.show()
sys.exit(app.exec())
```
## 5. Signal/Slot Hygiene
**Principle:** Maintain clean, readable, and robust signal/slot connections.
**Rule:** Use the modern `signal.connect(slot)` syntax. Avoid lambda-heavy connections inside loops; use `functools.partial` or dedicated methods for passing arguments. Encapsulate complex slot logic in separate, well-named methods.
❌ **BAD:** Old-style connections, lambdas in loops leading to closure issues.
```python
# Bad: Old-style connect, lambda in loop (i will always be 4)
class MyWindow(QMainWindow):
def __init__(self):
super().__init__()
layout = QVBoxLayout()
for i in range(5):
button = QPushButton(f"Button {i}")
layout.addWidget(button)
# button.clicked.connect(lambda: self.handle_button(i)) # 'i' is captured by reference!
# self.connect(button, SIGNAL("clicked()"), self.handle_button_old) # Old style
```
✅ **GOOD:** New-style connections, proper argument handling with `functools.partial`.
```python
# Good: New-style, proper argument handling
from PySide6.QtWidgets import QApplication, QMainWindow, QPushButton, QVBoxLayout, QWidget
from PySide6.QtCore import Slot
from functools import partial
import sys
class MyWindow(QMainWindow):
def __init__(self):
super().__init__()
central_widget = QWidget()
self.setCentralWidget(central_widget)
layout = QVBoxLayout(central_widget)
for i in range(5):
button = QPushButton(f"Button {i}")
layout.addWidget(button)
# Use functools.partial for arguments in loops to capture 'i' correctly
button.clicked.connect(partial(self._handle_button_with_arg, i))
self.another_button = QPushButton("Simple Action")
layout.addWidget(self.another_button)
self.another_button.clicked.connect(self._handle_simple_action)
@Slot(int)
def _handle_button_with_arg(self, index: int) -> None:
print(f"Button {index} clicked!")
# Delegate complex logic to other methods if needed
@Slot()
def _handle_simple_action(self) -> None:
print("Simple action triggered!")
self._perform_complex_sub_action()
def _perform_complex_sub_action(self) -> None:
# ... actual complex logic ...
print("Complex sub-action completed.")
if __name__ == "__main__":
app = QApplication(sys.argv)
window = MyWindow()
window.show()
sys.exit(app.exec())
```
## 6. Performance & Concurrency
**Principle:** Maintain a responsive UI by offloading long-running tasks from the main thread.
**Rule:** Never perform blocking I/O (e.g., network requests, file operations) or
© sanjeed5 · CC0-1.0 · sanjeed5/awesome-cursor-rules-mdc