2026, Jan 07 09:00

Preventing RuntimeError during PyQt6 logging shutdown: robust Handler design, flushOnClose, and a safe signal relay

PyQt6 logging bridge causing RuntimeError at shutdown? Fix it: define flushOnClose on your Python logging Handler or use a QObject signal relay for clean exits.

Bridging Python’s logging subsystem into a PyQt GUI is a common pattern: you forward LogRecord objects through a pyqtSignal and display them in widgets. It works—until shutdown, where a seemingly harmless attribute check triggers a RuntimeError because a Qt object has already been destroyed. Let’s unpack the root cause and fix it properly without hacks.

Problem overview

When a custom logging.Handler also inherits from QObject and emits a signal with the log record, application exit can raise an error during logging shutdown:

File "C:\Program Files\Python313\Lib\logging\__init__.py", line 2242, in shutdown
    if getattr(h, 'flushOnClose', True):
RuntimeError: wrapped C/C++ object of type Log2Qt has been deleted

Removing the handler on QApplication.aboutToQuit does not prevent the exception. The logger reports an empty handlers list, yet the shutdown sequence still reaches code that attempts to access the handler and, crucially, touches Qt-bound attributes on an already destroyed QObject.

Minimal reproducible example

The following snippet forwards log records to the GUI via a pyqtSignal using a handler that inherits from both QObject and logging.Handler. It also removes the handler on application quit, but the shutdown error still occurs.

import logging
from PyQt6.QtWidgets import QApplication, QWidget
from PyQt6.QtCore import QObject, pyqtSignal
class GuiLogBridge(QObject, logging.Handler):
    message_out = pyqtSignal(logging.LogRecord)
    def emit(self, rec):
        self.message_out.emit(rec)
log = logging.getLogger(__name__)
hnd = GuiLogBridge()
log.addHandler(hnd)
def on_app_quit():
    log.removeHandler(hnd)
    print(log.handlers)
app = QApplication([])
app.aboutToQuit.connect(on_app_quit)
window = QWidget()
window.show()
app.exec()

What’s actually going on

The failure stems from multiple inheritance combined with PyQt6’s attribute access semantics. During logging.shutdown, the logging module checks a handler attribute named flushOnClose using getattr. That attribute is not defined by default on a generic Handler; it only exists in specific subclasses such as MemoryHandler. In PyQt6, attribute lookup that crosses into Qt’s side is problematic once the QObject is destroyed. The getattr call performed during shutdown is enough to trigger the access on a dead QObject and raise the RuntimeError. This difference does not show up with PyQt5 due to a different access approach, which is why the same pattern might appear to work there.

Disconnecting aboutToQuit and removing the handler is not sufficient, because the issue is not about the handler still being attached for message delivery; it’s the attribute lookup on an object that Qt has already deleted. The timing and attribute resolution path are the culprits.

Fixes that actually work

One blunt approach is to delete the handler reference, ensuring the PyQt side goes away before logging inspects it during shutdown. This avoids the later getattr.

def on_app_quit():
    global hnd
    log.removeHandler(hnd)
    del hnd

While effective, this is not ideal. It requires guarantees about references elsewhere and does not address the underlying attribute access issue.

A cleaner solution is to define flushOnClose explicitly on the handler class. Since logging assumes True by default for that attribute, making it a plain Python attribute prevents getattr from reaching into the Qt layer after the QObject has been destroyed.

class GuiLogBridge(QObject, logging.Handler):
    message_out = pyqtSignal(logging.LogRecord)
    flushOnClose = True
    def emit(self, rec):
        self.message_out.emit(rec)

This sidesteps the problematic attribute resolution path entirely and keeps the multiple inheritance structure intact.

If you prefer to eliminate the multiple inheritance vector altogether, decouple the signal carrier from the logging handler. Create a QObject that only holds the signal and let a plain logging.Handler own an instance of that QObject. Emit through that instance in emit().

class SignalRelay(QObject):
    message_out = pyqtSignal(logging.LogRecord)
class GuiLogBridge(logging.Handler):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.relay = SignalRelay()
    def emit(self, rec):
        self.relay.message_out.emit(rec)

This version avoids QObject in the handler’s MRO and also behaves well with PySide, where QObject exposes an emit() method that could otherwise collide. With either of the last two approaches, there is no need to connect aboutToQuit for handler cleanup.

Why this matters

Logging and GUI frameworks have independent lifecycles, and their teardown sequences are not synchronized. Subtle differences in binding behavior, such as those between PyQt6 and PyQt5, can expose these edges. When attribute resolution crosses language boundaries into Qt internals, destroyed objects become a liability. Solid shutdown behavior prevents noisy exceptions and keeps application exit deterministic, which is essential for clean testing, automation, and production stability.

Takeaways

If you integrate Python logging with a PyQt6 UI by emitting LogRecord objects, be mindful of how attributes are resolved during shutdown. Defining flushOnClose on the handler is a minimal, direct fix that prevents getattr from poking at a dead QObject. If you want to avoid the pitfalls of multiple inheritance in this context, use a dedicated QObject for the signal and hold it inside a plain logging.Handler. Both approaches eliminate the need for aboutToQuit cleanup and result in a quiet, predictable shutdown. Multiple inheritance can be perfectly valid in theory, but with complex bindings it often introduces delicate interactions. Setting a class attribute solves this specific issue; separating concerns with a signal relay makes the design more resilient.