What are the best practices for building stable, crash-resistant Python and PyQt6 GUI applications, especially under real-world pressure from (Q)Threads and signal-slot logic and/or Multiprocessing (which introduces cross-process state hazards on Windows). Beware that imported libraries may be using native C threads or async IO without informing you!
We make heavy use of threading, especially when building and training models and sometimes also multiprocessing, for instances when downloading lots of data.
Guard All Entry Points
All code execution should be started from a special block that permits to run a Python code module that may be part of an application as a stand alone script:
if __name__ == “__main__”:
This is absolutely required for multiprocessing (especially on Windows), when using spawn-based libraries and with PyQt6 for QApplication instantiation safety.
Design (Q)Threads Carefully
What is true for threading in general should also be used with PyQt. Use either a worker objectin a QThread pattern or use QRunnable and QThreadPool if you don’t need signals/slots communication with the Main Window GUI.
Always move the worker to the thread: worker.moveToThread(thread), connect signals before thread.start() and avoid long processing in a GUI thread.
Implement closeEvent() to be able to wait for things to finish in their own time: self.thread_pool.waitForDone(timeout_ms
Avoid GUI Access from Worker Threads
Never call widget.update(), widget.setText(), etc. from background threads. Instead use pyqtSignal to send results back to the main thread and update the GUI in the connected slot within the main thread context.
Use try except
Wrap every thread entry with try/except even if it seems unlikely to fail:
def run(self):
try:
self.process_data()
except Exception as e:
self.progress_signal.emit(f”❌ Worker error: {e}”)
Handle Multiprocessing Explicitly
Never use multiprocessing without guarding with __main__. Use ProcessPoolExecutor only when necessary (e.g. for CPU-bound isolation). Always wrap future.result() in try/except and never assume global variables are shared (they aren’t with spawn).
Crash-Resistant UI Initialization
Chain startup logic via QTimer.singleShot(…) after QApplication is created. Avoid running anything expensive in the global module body to keep your GUI responsive. Wrap your Main Window setup with fallback checks:
if splash is not None:
splash.finish(window)
Catch Global Exceptions
That’s essential for capturing crashes from frameworks like Qt and for low-level code too. Best way to catch them, use a general exception hook, like this:
def my_exception_hook(exctype, value, tb):
traceback.print_exception(exctype, value, tb)
sys.__excepthook__(exctype, value, tb)
sys.excepthook = my_exception_hook
Graceful Exit & Finalization
Other safeguards and ways to find out what causes (otherwise) unexplainable error include the of atexit.register() to release resources (like models, sockets). To assure garbage collection, run gc.collect() manually after shutdown and log/print all threading.enumerate() at exit.
Avoid Race Conditions
Use staged init:
QTimer.singleShot(0, show_splash)
QTimer.singleShot(100, show_main_window)
Or even better: Chain show_main_window() at the end of show_splash().
Safe Use of Native Code
If using extensions like NumPy, TensorFlow, or Cython you should avoid long computations in main thread to keep it responsive. Beware that some libraries spawn non-daemon threads under the hood (e.g., OpenBLAS, MKL), these can block app shutdown if not handled carefully.
Summary of Safe Patterns
In general but certainly when using the GUI framework PyQt6 following these guidelines.
| ✅ DO |
| Use if __name__ == “__main__” as the starting point for all Qt and top-level logic. |
| Use freeze_support() when Multiprocessing on Windows. |
| Delay heavy Qt creation until QApplication exists. |
| Define show_main_window() globally but call it from inside __main__. |
| ❌ DON’T |
| Put splash screens or QTimer at module level. |
| Let subprocesses trigger Qt code. |
| Use global QApplication objects outside guarded code. |
| Start UI in worker subprocesses. |
Multiprocessing on Windows
With Multiprocessing beware that every time a ProcessPoolExecutor (or multiprocessing.Process) is created, Python re-launches a Python script from the top. This happens because Windows uses the spawn method for creating new processes — and spawn literally imports and runs the script anew. It is inspired by the Unix functions fork and exec to start a new child process. In Python the if __name__ == “__main__” guards matters, this block acts like a firewall, it ensures that code run only in the main launcher process, not in worker child processes.
Examples of defensive use of __main__ and closeEvent() methods
if __name__ == "__main__":
from multiprocessing import freeze_support
freeze_support()
app = QApplication(sys.argv)
splash = None
QTimer.singleShot(0, show_splash)
# moved to show splash QTimer.singleShot(100, show_main_window)
print("🟢 Starting Qt event loop...")
exit_code = app.exec()
print("🔴 Qt event loop ended with code:", exit_code)
import gc
gc.collect()
try:
sys.exit(exit_code)
except Exception as e:
print(f"Exception during application exit: {e}")
Python def closeEvent(self, event):
try:
print("🚪 Main window is closing...")
self.list_active_threads()
self.list_top_level_widgets()
print("🛑 Waiting for threads to shut down...")
self.thread_pool.waitForDone(1000)
finally:
import threading
for t in threading.enumerate():
print(f"Thread: {t.name}, daemon={t.daemon}, alive={t.is_alive()}")
QApplication.quit() # <- 🔴 force termination of Qt event loop
event.accept()
PythonThe Golden Checklist for Crash Resistant Python
| Technique | Why It’s Smart |
| if __name__ == “__main__” | Prevents multiprocessing chaos on Windows |
| freeze_support() | Required for multiprocessing start-up safety |
| closeEvent() with waitForDone() | Ensures graceful thread shutdown |
| Logging active threads at shutdown | Helps catch lingering daemon threads |
| try/finally in GUI event handlers | Prevents half-torn state on exceptions |
| atexit.register() cleanup | Captures exit logic even on fatal errors |
| Top-down signal flow (progress_signal.emit(…)) | Maintains separation between GUI and workers |
| Using QTimer.singleShot(…) to stage UI init | Avoids race conditions in UI bootstrapping |