Crash Resistant Python when using Threading and Multiprocessing

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()
Python

The Golden Checklist for Crash Resistant Python

TechniqueWhy 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 shutdownHelps catch lingering daemon threads
try/finally in GUI event handlersPrevents half-torn state on exceptions
atexit.register() cleanupCaptures 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 initAvoids race conditions in UI bootstrapping

Related Stories