pyqt-core · diff
v2.0.0 to v2.0.0
90 added, 396 removed. Audit A to A.
---
name: pyqt-core
description: Use when working with PyQt/PySide6 QtCore - signals, slots, properties, timers, settings, file I/O
metadata:
author: mte90
version: 2.0.0
tags:
- python
- qt
- pyqt
- pyside
- core
- signals
- slots
---
+ > **Objective:** Use this skill to reduce missed signal/slot connections, wrong connection-type crashes, QSettings scope mismatches, and timer leak incidents by enforcing connection-type rules for cross-thread communication, queue-aware patterns for worker threads, scope discipline for QSettings groups/arrays, and timer lifecycle checks with parent ownership.
+
# PyQt Core - QtCore Module
QtCore provides core non-GUI functionality: signals/slots, timers, settings, and file I/O.
- ## Overview
-
- QtCore is the foundation of Qt. It provides:
- - **Signal/Slot mechanism** - Type-safe event handling
- - **Properties** - Bindable object properties
- - **Timers** - Periodic and single-shot timers
- - **Settings** - Persistent application configuration
- - **File I/O** - Cross-platform file operations
-
- ## Signals and Slots
-
- ### Signal Declaration (PySide6)
-
- ```python
- from PySide6.QtCore import QObject, Signal
-
- class MyObject(QObject):
- # Define signals at class level
- valueChanged = Signal(int)
- nameChanged = Signal(str)
- dataReady = Signal(dict)
- errorOccurred = Signal(str)
-
- # Signal with multiple arguments
- positionChanged = Signal(int, int)
-
- # Signal with optional arguments
- progressChanged = Signal(int, arguments=['percent'])
- ```
-
- ### Signal Declaration (PyQt6)
-
- ```python
- from PyQt6.QtCore import QObject, pyqtSignal
-
- class MyObject(QObject):
- valueChanged = pyqtSignal(int)
- nameChanged = pyqtSignal(str)
- positionChanged = pyqtSignal(int, int)
- ```
-
- ### Slot Declaration (PySide6)
-
- ```python
- from PySide6.QtCore import Slot
+ ## Critical Rules (Do X Because Y)
- class MyObject(QObject):
- @Slot()
- def doSomething(self):
- print("Action performed")
-
- @Slot(int)
- def setValue(self, value):
- self._value = value
-
- @Slot(str, int)
- def processData(self, name, count):
- pass
-
- @Slot(result=str) # Return type annotation
- def getName(self) -> str:
- return self._name
- ```
+ ### 1. Use QueuedConnection for Cross-Thread Signals
- ### Slot Declaration (PyQt6)
+ **Do** use `Qt.QueuedConnection` when emitting signals from worker threads to UI slots because direct connections execute in the emitter's thread, causing crashes when slots access thread-unsafe UI objects.
```python
- from PyQt6.QtCore import pyqtSlot
+ from PySide6.QtCore import Qt, QObject, Signal, QThread
- class MyObject(QObject):
- @pyqtSlot()
- def doSomething(self):
- pass
+ class Worker(QThread):
+ dataReady = Signal(dict) # Declared in worker thread
- @pyqtSlot(int)
- def setValue(self, value):
- pass
- ```
-
- ### Connecting Signals to Slots
-
- ```python
- # Connect signal to slot
- button.clicked.connect(self.onButtonClick)
- valueChanged.connect(self.updateValue)
-
- # Connect with lambda
- button.clicked.connect(lambda: print("Clicked!"))
-
- # Connect with partial
- from functools import partial
- button.clicked.connect(partial(self.processItem, item_id))
-
- # Disconnect
- button.clicked.disconnect(self.onButtonClick)
-
- # Check connection
- is_connected = button.clicked.isConnected(self.onButtonClick)
-
- # Emit signal
- self.valueChanged.emit(42)
- self.positionChanged.emit(x, y)
-
- # Block signals temporarily
- button.blockSignals(True)
- button.setChecked(True)
- button.blockSignals(False)
-
- # Get signal emission count
- count = button.receivers(button.clicked)
- ```
-
- ### Connection Types
-
- ```python
- from PySide6.QtCore import Qt
-
- # Auto (default) - Direct if same thread, Queued if different
- button.clicked.connect(self.handleClick, Qt.AutoConnection)
-
- # Direct - Slot called immediately in signal's thread
- button.clicked.connect(self.handleClick, Qt.DirectConnection)
-
- # Queued - Slot called when control returns to receiver's thread
- worker.finished.connect(self.onFinished, Qt.QueuedConnection)
+ def run(self):
+ data = self.expensiveOperation()
+ self.dataReady.emit(data, Qt.QueuedConnection) # Queue to UI thread
- # BlockingQueued - Blocks until slot completes (use carefully!)
- worker.result.connect(self.handleResult, Qt.BlockingQueuedConnection)
+ class MainWindow(QWidget):
+ def setupWorker(self):
+ self.worker = Worker()
+ self.worker.dataReady.connect(self.handleData, Qt.QueuedConnection)
+ self.worker.start()
```
- ## Properties
-
- ### Property Declaration
-
- ```python
- from PySide6.QtCore import Property, Signal
+ **Failure mode:** Segmentation fault or Qt runtime error "QMetaObject::invokeMethod: Unable to unmarshal value" when DirectConnection crosses thread boundary.
- class Person(QObject):
- nameChanged = Signal()
-
- def __init__(self):
- super().__init__()
- self._name = ""
-
- @Property(str, notify=nameChanged)
- def name(self):
- return self._name
-
- @name.setter
- def name(self, value):
- if self._name != value:
- self._name = value
- self.nameChanged.emit()
- ```
+ ### 2. Block Signals During Batch Updates
- ### Using Properties
+ **Do** call `blockSignals(True)` before batch updates because each signal emission triggers connected slots, causing redundant processing and performance degradation.
```python
- person = Person()
- person.name = "Alice" # Setter called
- print(person.name) # Getter called
-
- # Connect to property change
- person.nameChanged.connect(lambda: print("Name changed!"))
+ def populateTable(self, items):
+ self.table.blockSignals(True)
+ try:
+ self.table.setRowCount(len(items))
+ for i, item in enumerate(items):
+ self.table.setItem(i, 0, QTableWidgetItem(item))
+ finally:
+ self.table.blockSignals(False)
```
- ## QTimer
-
- ### Single-Shot Timer
-
- ```python
- from PySide6.QtCore import QTimer
-
- # Call function after 1000ms
- QTimer.singleShot(1000, self.onTimer)
-
- # With lambda
- QTimer.singleShot(500, lambda: print("Delayed!"))
+ **Failure mode:** Slot called N times for N row additions instead of once after batch complete.
- # Cancel single-shot (store reference)
- self.timer_id = QTimer.singleShot(1000, self.delayedAction)
- # Note: Can't cancel singleShot, use regular timer instead
- ```
+ ### 3. Use Parent Ownership for Timers
- ### Periodic Timer
+ **Do** pass parent to `QTimer(parent)` because parent ownership ensures automatic cleanup on widget destruction, preventing timer leaks and crashes from dangling timer references.
```python
- from PySide6.QtCore import QTimer
-
class MyWidget(QWidget):
def __init__(self):
super().__init__()
-
- # Create timer
- self.timer = QTimer(self)
+ self.timer = QTimer(self) # Self is parent - auto-cleanup
self.timer.timeout.connect(self.onTimeout)
-
- # Start with 100ms interval
- self.timer.start(100)
-
- # Or: self.timer.setInterval(100); self.timer.start()
-
- def onTimeout(self):
- print("Timer fired!")
-
- def stopTimer(self):
- self.timer.stop()
-
- def isRunning(self):
- return self.timer.isActive()
-
- def remainingTime(self):
- return self.timer.remainingTime() # ms until next timeout
```
- ### Timer Properties
-
- ```python
- timer = QTimer()
-
- # Interval (milliseconds)
- timer.setInterval(1000) # 1 second
- interval = timer.interval()
-
- # Single-shot mode
- timer.setSingleShot(True) # Fires once then stops
-
- # Timer type
- timer.setTimerType(Qt.PreciseTimer) # ~1ms accuracy
- timer.setTimerType(Qt.CoarseTimer) # ~5% accuracy (default)
- timer.setTimerType(Qt.VeryCoarseTimer) # ~500ms accuracy
- ```
+ **Failure mode:** Timer continues firing after widget destroyed → crash on slot access.
- ## QSettings
+ ### 4. Provide Defaults When Reading Settings
- ### Basic Usage
+ **Do** provide default values in `settings.value(key, default)` because missing keys return `None` without defaults, causing type errors downstream.
```python
- from PySide6.QtCore import QSettings
-
- # Create settings (uses app name from QApplication)
- settings = QSettings("MyCompany", "MyApp")
-
- # Write values
- settings.setValue("window/geometry", self.saveGeometry())
- settings.setValue("window/state", self.saveState())
- settings.setValue("editor/fontSize", 12)
- settings.setValue("editor/fontFamily", "Monospace")
-
- # Read values
- geometry = settings.value("window/geometry")
- font_size = settings.value("editor/fontSize", 10) # Default 10
- font_family = settings.value("editor/fontFamily", "Consolas")
-
- # Check if key exists
- if settings.contains("editor/theme"):
- theme = settings.value("editor/theme")
-
- # Remove key
- settings.remove("editor/temp")
+ # Safe - returns int 12 if key missing
+ font_size = settings.value("editor/fontSize", 10, type=int)
- # Clear all
- settings.clear()
+ # Unsafe - returns None if missing, crashes on int comparison
+ font_size = settings.value("editor/fontSize") # type: ignore
```
- ### Settings Groups
-
- ```python
- settings = QSettings("MyCompany", "MyApp")
-
- # Using beginGroup/endGroup
- settings.beginGroup("editor")
- settings.setValue("fontSize", 12)
- settings.setValue("fontFamily", "Monospace")
- settings.endGroup()
-
- # Using array
- settings.beginWriteArray("recentFiles")
- for i, filepath in enumerate(recent_files):
- settings.setArrayIndex(i)
- settings.setValue("path", filepath)
- settings.endArray()
+ **Failure mode:** `TypeError: '>' not supported between instances of 'NoneType' and 'int'`.
- # Read array
- size = settings.beginReadArray("recentFiles")
- for i in range(size):
- settings.setArrayIndex(i)
- path = settings.value("path")
- settings.endArray()
- ```
+ ### 5. Use IniFormat for Portable Settings
- ### Settings Format
+ **Do** use `QSettings.IniFormat` for cross-platform config files because NativeFormat stores in platform-specific locations (Windows registry, macOS plist), making settings non-portable.
```python
- from PySide6.QtCore import QSettings
-
- # Native format (registry on Windows, plist on macOS, conf on Linux)
- settings = QSettings("MyCompany", "MyApp")
-
- # INI file format
+ # Portable - same file on all platforms
settings = QSettings("config.ini", QSettings.IniFormat)
- # Custom file location
- settings = QSettings("/path/to/settings.conf", QSettings.IniFormat)
-
- # Get file path
- print(settings.fileName())
+ # Platform-specific - different storage per OS
+ settings = QSettings("MyCompany", "MyApp") # Native format
```
- ## File I/O
-
- ### QFile
-
- ```python
- from PySide6.QtCore import QFile, QIODevice
-
- # Read file
- file = QFile("data.txt")
- if file.open(QIODevice.ReadOnly | QIODevice.Text):
- data = file.readAll()
- file.close()
- print(data)
-
- # Write file
- file = QFile("output.txt")
- if file.open(QIODevice.WriteOnly | QIODevice.Text):
- file.write(b"Hello, World!")
- file.close()
-
- # Append
- file = QFile("log.txt")
- if file.open(QIODevice.Append | QIODevice.Text):
- file.write(b"New log entry\n")
- file.close()
-
- # Check exists
- if QFile.exists("data.txt"):
- print("File exists")
-
- # Remove file
- QFile.remove("temp.txt")
-
- # Copy file
- QFile.copy("source.txt", "dest.txt")
+ **Failure mode:** Settings appear "lost" when moving between platforms or after OS reinstall.
- # Rename file
- QFile.rename("old.txt", "new.txt")
- ```
+ ### 6. End Groups/Arrays in Finally Blocks
- ### QTextStream
+ **Do** wrap `beginGroup/endGroup` and `beginWriteArray/endArray` in try-finally because exceptions leave settings in incomplete state, corrupting the file.
```python
- from PySide6.QtCore import QFile, QTextStream, QIODevice
-
- # Read text
- file = QFile("data.txt")
- if file.open(QIODevice.ReadOnly | QIODevice.Text):
- stream = QTextStream(file)
- stream.setEncoding(QTextStream.Utf8)
-
- # Read all
- content = stream.readAll()
-
- # Read line by line
- while not stream.atEnd():
- line = stream.readLine()
- print(line)
-
- file.close()
-
- # Write text
- file = QFile("output.txt")
- if file.open(QIODevice.WriteOnly | QIODevice.Text):
- stream = QTextStream(file)
- stream.setEncoding(QTextStream.Utf8)
- stream << "Line 1\n"
- stream << "Line 2\n"
- stream.writeString("Unicode: 你好")
- file.close()
+ def saveSettings(self):
+ settings = QSettings("config.ini", QSettings.IniFormat)
+ try:
+ settings.beginGroup("editor")
+ settings.setValue("fontSize", 12)
+ settings.endGroup()
+ finally:
+ settings.endGroup() # Ensures cleanup even on error
```
- ### QDir
+ **Failure mode:** Corrupted INI file with unclosed groups preventing future reads.
- ```python
- from PySide6.QtCore import QDir
+ ### 7. Stop Timers Before Destruction
- # Current directory
- current = QDir.currentPath()
- home = QDir.homePath()
- temp = QDir.tempPath()
+ **Do** explicitly stop timers in `closeEvent` because while parent ownership handles cleanup, stopping timers prevents late emissions during destruction sequence.
- # Create directory
- dir = QDir()
- dir.mkpath("/path/to/new/directory") # Creates all parent dirs
+ ```python
+ def closeEvent(self, event):
+ self.timer.stop() # Stop before parent cleanup
+ super().closeEvent(event)
+ ```
- # Check if exists
- if QDir("/path/to/dir").exists():
- print("Directory exists")
+ **Failure mode:** Timer fires during widget teardown → slot accesses destroyed objects.
- # List directory contents
- dir = QDir("/path/to/dir")
- entries = dir.entryList() # Files and dirs
- files = dir.entryList(QDir.Files) # Only files
- dirs = dir.entryList(QDir.Dirs | QDir.NoDotAndDotDot) # Only subdirs
+ ### 8. Check File Existence Before Operations
- # With filters
- entries = dir.entryList(["*.txt"], QDir.Files | QDir.Readable)
+ **Do** check `QFile.exists()` before read operations because `open()` silently fails on missing files, causing data loss if write mode is used accidentally.
- # Remove directory
- QDir("/path/to/dir").rmdir(".") # Must be empty
+ ```python
+ if QFile.exists("data.txt"):
+ file = QFile("data.txt")
+ if file.open(QIODevice.ReadOnly):
+ data = file.readAll()
+ file.close()
+ else:
+ logger.warning("Data file not found, using defaults")
```
- ## Best Practices
+ **Failure mode:** Silent data loss when file missing and code proceeds with empty data.
- ### 1. Use Type Hints
+ ## Signal/Slot Reference
- ```python
- from PySide6.QtCore import Signal, Slot
+ See `references/signals-slots.md` for complete signal declaration, slot declaration, and connection syntax.
- class MyObject(QObject):
- valueChanged = Signal(int)
-
- @Slot(int)
- def setValue(self, value: int) -> None:
- self._value = value
- ```
+ ## QTimer Reference
- ### 2. Clean Up Timers
+ See `references/timers.md` for complete timer API including single-shot, periodic, and timer properties.
- ```python
- class MyWidget(QWidget):
- def __init__(self):
- super().__init__()
- self.timer = QTimer(self) # Parent ensures cleanup
-
- def closeEvent(self, event):
- self.timer.stop()
- super().closeEvent(event)
- ```
+ ## QSettings Reference
- ### 3. Use Settings Defaults
+ See `references/settings.md` for complete settings API including groups, arrays, and format options.
- ```python
- # Always provide sensible defaults
- font_size = settings.value("fontSize", 12, type=int)
- theme = settings.value("theme", "dark")
- ```
+ ## File I/O Reference
- ### 4. Close Files
+ See `references/file-io.md` for complete file operations including QFile, QTextStream, and QDir.
- ```python
- # Use context manager pattern
- file = QFile("data.txt")
- if file.open(QIODevice.ReadOnly):
- try:
- data = file.readAll()
- finally:
- file.close()
- ```
+ ## Deep Dives
+ - `references/signals-slots.md` - Signal/slot syntax, connection types, best practices
+ - `references/timers.md` - QTimer API, single-shot vs periodic, timer types
+ - `references/settings.md` - QSettings API, groups, arrays, formats
+ - `references/file-io.md` - QFile, QTextStream, QDir operations
+
## References
- **Qt for Python QtCore**: https://doc.qt.io/qtforpython-6/PySide6/QtCore/
- - **Signals and Slots**: https://doc.qt.io/qtforpython-6/signalslot.html
+ - **Signals and Slots**: https://doc.qt.io/qtforpython-6/signalsslot.html
- **QTimer**: https://doc.qt.io/qtforpython-6/PySide6/QtCore/QTimer.html
- **QSettings**: https://doc.qt.io/qtforpython-6/PySide6/QtCore/QSettings.html
- **QFile**: https://doc.qt.io/qtforpython-6/PySide6/QtCore/QFile.html