---
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:
    # ...
```
