mcpField
P
pyqt-threading

pyqt-threading

github.com/codeatcodeDevelopment2747 installsUpdated today
Repository

About pyqt-threading

A reference skill for developers building threaded PyQt or PySide6 applications. It explains worker-object patterns, safe UI communication, synchronization, and thread cleanup.

Key features

  1. 01Worker-object pattern with QThread
  2. 02Signal-based cross-thread communication
  3. 03Thread lifecycle and cleanup guidance
  4. 04Locking and race-condition guidance
  5. 05References for testing and advanced patterns

Use cases

  1. 01Move long-running work off the UI thread
  2. 02Update widgets safely from worker threads
  3. 03Diagnose deadlocks and race conditions

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/codeatcode/oss-ai-skills --skill pyqt-threading

After installing, type/pyqt-threadingin Claude Code to confirm it loaded.

Bundled files

References and scripts installed along with SKILL.md.

references/
advanced-safety.md
lifecycle-cleanup.md
pool-patterns.md
qt-concurrent.md
testing.md

Details

From the project README

PyQt Threading - Core Patterns

Thread Safety Rules

CRITICAL: Qt/PyQt is NOT thread-safe for UI operations. You MUST follow these rules:

  1. Never access widgets from worker threads - Only the main thread can modify UI
  2. Use signals for cross-thread communication - Emit signals from worker, connect to slots in main thread
  3. Use Qt.QueuedConnection for thread-safe signal delivery - AutoConnection handles this automatically
  4. Never block the main thread - Long operations will freeze the UI
# ❌ WRONG: Direct UI access from thread
class BadWorker(QThread):
    def run(self):
        # This will crash or cause undefined behavior!
        self.label.setText("Done")

# ✅ CORRECT: Use signals
class GoodWorker(QThread):
    finished = Signal(str)
    
    def run(self):
        result = self.process_data()
        self.finished.emit(result)  # Signal emitted, UI updated in main thread

QThread with Worker Object (Recommended Pattern)

The most flexible pattern separates the worker logic from thread lifecycle:

from PySide6.QtCore import QThread, Signal, QObject, Slot

class Worker(QObject):
    """Worker object that does the actual work."""
    finished = Signal(object)
    progress = Signal(int)
    error = Signal(str)
    
    def __init__(self, data):
        super().__init__()
        self.data = data
        self._is_cancelled = False
    
    @Slot()
    def process(self):
        """Main processing method called from thread."""
        try:
            for i, item in enumerate(self.data):
                if self._is_cancelled:
                    return
                
                # Simulate heavy work
                result = self.process_item(item)
                self.progress.emit(int((i + 1) / len(self.data) * 100))
            
            self.finished.emit({"status": "success", "count": len(self.data)})
        except Exception as e:
            self.error.emit(str(e))
    
    def cancel(self):
        self._is_cancelled = True
    
    def process_item(self, item):
        import time
        time.sleep(0.1)  # Simulate work
        return item * 2

class ThreadController(QObject):
    """Manages worker thread lifecycle."""
    def __init__(self):
        super().__init__()
        self.thread = None
        self.worker = None
    
    def start_work(self, data):
        # Create thread and worker
        self.thread = QThread()
        self.worker = Worker(data)
        
        # Move worker to thread
        self.worker.moveToThread(self.thread)
        
        # Connect signals
        self.worker.finished.connect(self.on_finished)
        self.worker.progress.connect(self.on_progress)
        self.worker.error.connect(self.on_error)
        
        # Thread lifecycle
        self.thread.started.connect(self.worker.process)
        self.thread.finished.connect(self.thread.deleteLater)
        
        # Start thread
        self.thread.start()
    
    def cancel_work(self):
        if self.worker:
            self.worker.cancel()
        if self.thread:
            self.thread.quit()
            self.thread.wait()
    
    @Slot()
    def on_finished(self, result):
        print(f"Work completed: {result}")
        self.cleanup()
    
    @Slot()
    def on_progress(self, percent):
        print(f"Progress: {percent}%")
    
    @Slot()
    def on_error(self, error):
        print(f"Error: {error}")
        self.cleanup()
    
    def cleanup(self):
        self.thread = None
        self.worker = None

moveToThread() Pattern - Worker Object Semantics

Correct Pattern: Worker + Thread Controller

The worker object pattern is the recommended approach for explicit thread control:

from PySide6.QtCore import QThread, Signal, QObject, Slot

class Worker(QObject):
    """Worker owns the work, controller owns the thread."""
    progress = Signal(int)
    finished = Signal(object)
    
    def __init__(self, data):
        super().__init__()
        self.data = data
        self._is_running = False
    
    @Slot()
    def process(self):
        """Main work method - called from thread."""
        self._is_running = True
        try:
            # Heavy computation here
            for i in range(100):
                self.progress.emit(i)
            self.finished.emit(None)
        finally:
            self._is_running = False

class ThreadController(QObject):
    """Controller owns thread and manages worker."""
    def __init__(self):
        super().__init__()
        self.thread = None
        self.worker = None
    
    def start_work(self, data):
        # Create NEW thread and worker
        self.thread = QThread()
        self.worker = Worker(data)
        
        # CRITICAL: Move worker TO thread
        self.worker.moveToThread(self.thread)
        
        # Worker lifetime tied to thread lifetime
        self.thread.started.connect(self.worker.process)
        self.thread.finished.connect(self.thread.quit)
        self.thread.finished.connect(self.thread.deleteLater)
        
        self.thread.start()
Ownership Semantics
ObjectOwnerLifetimeDeletion
WorkerThreadControllerUntil thread.quit() + wait()worker.deleteLater()
ThreadThreadControllerUntil deletedthread.deleteLater()
SignalsTheir parentUntil parent deletedAutomatic
Common Mistakes to Avoid
# ❌ WRONG: Worker outlives thread
controller = ThreadController()
controller.thread = QThread()
controller.worker = Worker()
controller.thread.moveToThread(controller.worker)  # Wrong direction!
controller.thread.start()
# Problem: Worker destroyed before thread finishes

# ❌ WRONG: Forgetting thread lifecycle
def start_work():
    thread = QThread()
    worker = Worker()
    worker.moveToThread(thread)
    thread.start()  # Never quit() or wait()!
# Problem: Zombie thread keeps running

# ✅ CORRECT: Proper ownership chain
controller = ThreadController()
controller.start_work(data)
# Later when done:
controller.thread.quit()
controller.thread.wait()
# Now delete: controller.worker.deleteLater()

Common Issues

IssueCauseSolution
UI freezesBlocking operation in main threadMove to worker thread
Crashes on widget accessAccessing UI from worker threadUse signals instead
Memory leaksThread not cleaned upUse deleteLater() and proper lifecycle
DeadlocksMultiple mutexes acquired in different orderAlways acquire in same order, use timeout
Race conditionsShared data without locksUse QMutex or atomic operations

Best Practices

  1. Always use signals for cross-thread communication - Direct widget access from threads causes crashes
  2. Keep worker objects thread-affinity aware - Never assume QObject is in main thread
  3. Clean up threads properly - Use deleteLater() and quit() + wait()
  4. Handle cancellation - Check flags periodically in long operations
  5. Use QThreadPool for parallel independent tasks - Default pool manages resource limits
  6. Use moveToThread() for explicit thread control - Worker object pattern is recommended
  7. Never use time.sleep() in main thread - Use QTimer or workers instead
  8. Keep locks short-lived - Only hold mutexes for critical section duration
  9. Use QReadWriteLock for read-heavy data - Multiple readers possible, single writer
  10. Monitor thread pool limits - Set maxThreadCount to prevent resource exhaustion

Common Pitfalls

IssueCauseSolution
UI crashes on widget accessWidget accessed from worker threadAlways use signals to update UI from main thread
DeadlockMultiple mutexes acquired in different orderAlways acquire in consistent order, use timeouts
Race conditionsShared data without locksUse QMutex, QAtomicPointer, or atomic operations
Memory leaksThreads not cleaned upUse deleteLater() on threads and workers
Thread not stoppingNo quit() + wait() sequenceAlways call thread.quit() then thread.wait()
Signals firing from wrong threadAutoConnection uses queued deliveryAutoConnection is correct - don't change
Re-entrancy issuesSignal handler calls slot recursivelyUse flags to track state changes
Resource exhaustionUnlimited thread pool threadsSet maxThreadCount on QThreadPool
Busy-wait loopsThread polling without sleepUse QTimer instead of polling
Lock not releasedException before unlockUse QMutexLocker for RAII-style cleanup
Top 5 Mistakes to Avoid
  1. Never access UI from worker threads - The most common crash cause

    # ❌ CRASH: Direct UI access
    def run(self):
        self.label.setText("Done")  # Crashes!
    
    # ✅ SAFE: Use signals
    def run(self):
        self.finished.emit("Done")  # UI updated in main thread
    
  2. Forgetting thread lifecycle management - Threads become zombies

    # ❌ BAD: No cleanup
    thread = QThread()
    thread.start()  # Never quit() or wait()
    
    # ✅ GOOD: Proper lifecycle
    thread.start()
    thread.quit()
    thread.wait()  # Wait for thread to finish
    
  3. Acquiring locks in wrong order - Deadlock

    # ❌ DEADLOCK
    def do_work(self):
        with lock_a:
            with lock_b:
                pass
    
    def do_other_work(self):
        with lock_b:  # Different order!
            with lock_a:
                pass
    
    # ✅ SAFE: Consistent ordering
    # Always acquire locks in same order (e.g., by ID)
    
  4. Using locks too long - Performance issues

    # ❌ BAD: Lock held for long operation
    with lock:
        result = heavy_computation()
    
    # ✅ GOOD: Hold lock only for data access
    with lock:
        data = self.shared_data
    result = heavy_computation(data)  # Outside lock
    
  5. Forgetting to check cancellation - Threads don't stop

    # ❌ BAD: Infinite loop
    def run(self):
        while True:
            do_work()
    
    # ✅ GOOD: Check cancellation flag
    def run(self):
        while not self._cancelled:
            if self._work_done():
                break
            do_work()
    

Deep Dives

For detailed coverage of advanced patterns, load these reference files:

  • Pool Patterns (references/pool-patterns.md) - QThread Subclass, QThreadPool/QRunnable, QTimer, Fire-and-Forget
  • Qt Concurrent (references/qt-concurrent.md) - Map/filter/reduce operations, progress tracking, simple background tasks
  • Lifecycle & Cleanup (references/lifecycle-cleanup.md) - Thread signals, proper cleanup, graceful shutdown
  • Advanced Safety (references/advanced-safety.md) - Thread-safe state, UI state races, resource exhaustion
  • Testing (references/testing.md) - pytest-qt patterns, signal testing, thread lifecycle testing, race condition tests

References

Official Documentation
Qt for Python (PySide6/PyQt6)
Community Resources
Advanced Topics
Testing

Comments

Markdown supported