pyqt — Cursor rule: what it does, how to install
A Cursor rule that enforces clean PyQt application design: dynamic UI loading, Model-View separation, and background processing via thread pools.
.cursor/rules/pyqt.mdcWhat pyqt does
Applied to **/*.py files, this rule guides Cursor to write structured PyQt6 code. It mandates designing layouts visually in Qt Designer and loading .ui files at runtime with loadUi or single inheritance. Modifying files produced by pyuic by hand is strictly discouraged. It breaks maintainability when layouts get recompiled.
Data presentation relies on the Model-View pattern. Data belongs inside custom QAbstractListModel classes rather than widget items like QListWidget. Heavy operations must run off the main thread using QThreadPool and QRunnable. Running slow tasks on the UI thread freezes the window, so workers communicate back exclusively through pyqtSignal.
What it helps with
- stop manual edits inside pyuic generated UI files
- decouple presentation from data with QAbstractListModel
- offload heavy blocking tasks to QThreadPool and QRunnable
- connect application logic with UI events through signals and slots
Example prompts for the agent
- Create a PyQt6 window that dynamically loads mainwindow.ui and sets up signal connections
- Implement a background worker using QRunnable and QThreadPool to fetch data without freezing the UI
How to install pyqt
Manually: put the files in place
.cursor/rules/pyqt.mdcFrontmatter: description, globs, alwaysApply
Manually: put the files in place
.claude/rules/pyqt.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/pyqt.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/pyqt.mdManually: put the files in place
.clinerules/pyqt.mdStep by step, with commands for each platform: Cursor rules: .mdc files, four modes and what replaced .cursorrules
Questions about pyqt
How to install the pyqt rule in Cursor?
Save the rule file to .cursor/rules/pyqt.mdc in your workspace root. Cursor will pick it up automatically for any **/*.py files.
What does the pyqt rule enforce for UI layouts?
It requires using Qt Designer for layouts and loading .ui files dynamically via loadUi or inheriting the generated class. Directly editing pyuic output files is forbidden.
How does this rule handle background processing in PyQt?
It prevents blocking the main UI thread by routing slow routines into QThreadPool via QRunnable. Updates return to the main interface strictly through Qt signals.
Source text
pyqt.mdc · CC0-1.0
---
description: Enforce modern, maintainable, and performant PyQt application development standards by leveraging Qt Designer, Model-View architecture, and responsive threading.
globs: **/*.py
---
# PyQt Best Practices
This guide outlines essential practices for building robust, scalable, and maintainable PyQt applications. Adhere to these rules for consistent, high-quality code.
## 1. UI Design: Qt Designer & Dynamic Loading
Always design your UI visually with **Qt Designer** (or Qt Design Studio). Keep the generated `.ui` files pristine. Load them dynamically at runtime or integrate the `pyuic`-generated code via inheritance without modification. This separates design from logic and simplifies UI updates.
❌ **BAD: Hand-editing `pyuic`-generated files**
```python
# ui_mainwindow.py (DO NOT EDIT THIS FILE DIRECTLY)
# This file was generated by pyuic6...
class Ui_MainWindow(object):
def setupUi(self, MainWindow):
# ... generated code ...
self.my_custom_button.clicked.connect(self.my_logic_function) # Added here
```
✅ **GOOD: Dynamic loading with `QUiLoader` (PyQt6)**
```python
from PyQt6.QtWidgets import QApplication, QWidget
from PyQt6.uic import loadUi
class MyMainWindow(QWidget):
def __init__(self):
super().__init__()
loadUi("mainwindow.ui", self) # Load UI dynamically
self.my_button.clicked.connect(self._on_button_clicked)
def _on_button_clicked(self):
print("Button clicked!")
if __name__ == "__main__":
app = QApplication([])
window = MyMainWindow()
window.show()
app.exec()
```
✅ **GOOD: Single inheritance with `pyuic`-generated code**
```python
# ui_mainwindow.py (Generated by `pyuic6 -o ui_mainwindow.py mainwindow.ui`)
# DO NOT EDIT THIS FILE.
from PyQt6 import QtWidgets # ... other imports ...
class Ui_MainWindow(object):
def setupUi(self, MainWindow):
# ... generated UI setup code ...
pass # No signal connections here
# main.py (Your application logic)
from PyQt6.QtWidgets import QApplication, QMainWindow
from ui_mainwindow import Ui_MainWindow # Import generated UI
class MyMainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.ui = Ui_MainWindow()
self.ui.setupUi(self) # Set up the UI on this window
self.ui.my_button.clicked.connect(self._on_button_clicked)
def _on_button_clicked(self):
print("Button clicked from inherited UI!")
if __name__ == "__main__":
app = QApplication([])
window = MyMainWindow()
window.show()
app.exec()
```
## 2. Component Architecture: Model-View Separation
For any data-driven UI (lists, tables, trees), adopt the **Model-View architecture**. This decouples data management from UI presentation, leading to cleaner, more scalable code.
❌ **BAD: Storing data directly in widgets**
```python
from PyQt6.QtWidgets import QListWidget, QListWidgetItem
# ...
self.my_list_widget = QListWidget()
self.my_list_widget.addItem("Item 1")
self.my_list_widget.addItem("Item 2")
# To update: self.my_list_widget.item(0).setText("New Item 1") # Tedious
```
✅ **GOOD: Custom `QAbstractListModel` for data**
```python
from PyQt6.QtCore import QAbstractListModel, QModelIndex, Qt
from PyQt6.QtWidgets import QListView, QApplication
class MyListModel(QAbstractListModel):
def __init__(self, data=None):
super().__init__()
self._data = data or []
def data(self, index: QModelIndex, role: int):
if role == Qt.ItemDataRole.DisplayRole:
return self._data[index.row()]
return None
def rowCount(self, parent: QModelIndex):
return len(self._data)
def add_item(self, item: str):
self.beginInsertRows(QModelIndex(), len(self._data), len(self._data))
self._data.append(item)
self.endInsertRows()
# ... in your QMainWindow or QWidget
# self.my_list_view = QListView()
# self.model = MyListModel(["Initial Item 1", "Initial Item 2"])
# self.my_list_view.setModel(self.model)
# self.model.add_item("New Item from Logic") # Model updates view automatically
```
## 3. Asynchronous Operations: Keep UI Responsive
Never block the UI thread with long-running tasks. Use `QThreadPool` with `QRunnable` or `QThread` for background processing. Emit signals from worker threads to update the UI on the main thread.
❌ **BAD: Blocking UI thread**
```python
import time
from PyQt6.QtWidgets import QPushButton, QLabel
# ...
def on_heavy_button_clicked(self):
self.status_label.setText("Processing...")
time.sleep(5) # UI freezes for 5 seconds
self.status_label.setText("Done!")
```
✅ **GOOD: Offload to `QThreadPool`**
```python
from PyQt6.QtCore import QRunnable, QThreadPool, pyqtSignal, QObject
from PyQt6.QtWidgets import QPushButton, QLabel, QApplication, QWidget
import time
class WorkerSignals(QObject):
finished = pyqtSignal()
result = pyqtSignal(str)
class Worker(QRunnable):
def __init__(self, task_id):
super().__init__()
self.signals = WorkerSignals()
self.task_id = task_id
def run(self):
time.sleep(2) # Simulate heavy work
self.signals.result.emit(f"Task {self.task_id} completed.")
self.signals.finished.emit()
class MyWindow(QWidget):
def __init__(self):
super().__init__()
self.threadpool = QThreadPool()
self.button = QPushButton("Start Heavy Task", self)
self.label = QLabel("Ready", self)
self.button.clicked.connect(self._start_task)
# ... layout setup ...
def _start_task(self):
self.label.setText("Processing...")
worker = Worker(time.time()) # Unique ID for task
worker.signals.result.connect(self.label.setText)
worker.signals.finished.connect(lambda: print("Worker finished"))
self.threadpool.start(worker)
if __name__ == "__main__":
app = QApplication([])
window = MyWindow()
window.show()
app.exec()
```
## 4. Communication: Signals & Slots
Leverage Qt's powerful signals and slots mechanism for decoupled communication between UI components and business logic.
❌ **BAD: Direct method calls for UI events**
```python
# In MainWindow
self.login_button.clicked.connect(self.login_manager.authenticate) # login_manager is tightly coupled
```
✅ **GOOD: Decoupled signals and slots**
```python
from PyQt6.QtCore import pyqtSignal, QObject
# ...
class LoginManager(QObject):
login_successful = pyqtSignal(str)
login_failed = pyqtSignal(str)
def authenticate(self, username, password):
if username == "user" and password == "pass":
self.login_successful.emit("Welcome!")
else:
self.login_failed.emit("Invalid credentials.")
# In MainWindow
# self.login_manager = LoginManager()
# self.login_manager.login_successful.connect(self.show_dashboard)
# self.login_manager.login_failed.connect(self.display_error)
# self.login_button.clicked.connect(lambda: self.login_manager.authenticate(self.user_input.text(), self.pass_input.text()))
```
## 5. Code Organization & Naming
Follow Pythonic naming conventions (`CamelCase` for Qt classes, `snake_case` for methods/variables). Organize code into logical modules (e.g., `ui/`, `models/`, `workers/`). Use Qt resource files (`.qrc`) for icons, images, and stylesheets.
❌ **BAD: Inconsistent naming, monolithic files**
```python
# myapp.py
class my_main_window(QtWidgets.QMainWindow): # Incorrect class name
def __init__(self):
self.MyButton = QtWidgets.QPushButton() # Incorrect variable name
# ... all logic here ...
```
✅ **GOOD: Consistent naming, modular structure**
```python
# main.py
from PyQt6.QtWidgets import QApplication
from my_app.ui.main_window import MainWindow # Separate UI module
if __name__ == "__main__":
app = QApplication([])
main_window = MainWindow()
main_window.show()
app.exec()
# my_app/ui/main_window.py
from PyQt6.QtWidgets import QMainWindow
# ...
class MainWindow(QMainWindow): # CamelCase for Qt classes
def __init__(self):
super().__init__()
self._setup_ui() # Private helper method
self._connect_signals()
def _on_button_clicked(self): # snake_case for methods
pass
# my_app/resources.qrc (compiled to my_app/resources_rc.py)
# <RCC>
# <qresource prefix="/icons">
# <file>icon.png</file>
# </qresource>
# </RCC>
# Use: self.setWindowIcon(QIcon(":/icons/icon.png"))
```
## 6. Styling & Theming
Use Qt's stylesheet system (CSS-like `.qss` files) for styling. This allows for easy theming (e.g., dark mode) without modifying Python code.
❌ **BAD: Hardcoding styles in Python**
```python
self.my_button.setStyleSheet("background-color: red; color: white;") # Hardcoded
```
✅ **GOOD: External stylesheets**
```python
# styles.qss
QPushButton {
background-color: #2196F3; /* Blue */
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
}
QPushButton:hover {
background-color: #1976D2; /* Darker Blue */
}
# In main.py or MainWindow __init__
with open("styles.qss", "r") as f:
_stylesheet = f.read()
app.setStyleSheet(_stylesheet) # Apply globally
# Or self.my_widget.setStyleSheet(_stylesheet) # Apply locally
```
## 7. Type Hints
Always use Python type hints for improved readability, maintainability, and static analysis.
❌ **BAD: Untyped code**
```python
def process_data(data):
# ...
```
✅ **GOOD: Type-hinted code**
```python
from typing import List, Tuple
from PyQt6.QtCore import QModelIndex
def process_data(data: List[Tuple[bool, str]]) -> List[str]:
# ...
return [item[1] for item in data]
def data(self, index: QModelIndex, role: int) -> str | None:
# ...
```
© sanjeed5 · CC0-1.0 · sanjeed5/awesome-cursor-rules-mdc