2025, Dec 09 23:00

PyQt6 vs PyQt5 Enums: Why QMessageBox.Ok Appears to Work, How QtPy Patches It, and the Correct Qt6 Namespace Usage

Seeing PyQt5-style QMessageBox.Ok work under PyQt6? It’s QtPy patching, not a hidden fallback. Learn why it happens and fix it with Qt6 enum namespaces.

Why does PyQt5-style QMessageBox code seem to work under PyQt6? If you’ve seen legacy enum access like QMessageBox.Ok and QMessageBox.Information behave as if nothing changed, the instinctive explanation is a hidden fallback in PyQt6. There isn’t one. The reason lies elsewhere.

Problem setup

Two Python applications run in the same virtual environment (PyQt6.5, Python 3.11). One uses the new PyQt6 enum namespaces and works as expected. The other uses the old PyQt5-style enums, yet still runs without PyQt5 installed. That’s surprising, because PyQt6 normally requires qualified enum namespaces.

Here is the PyQt6-style usage:

alertDlg = QMessageBox(parent=self)
alertDlg.setText('WHOOPS - Must have at least one (1) joystick attached')
alertDlg.setStandardButtons(QMessageBox.StandardButton.Ok)
alertDlg.setIcon(QMessageBox.Icon.Warning)
alertDlg.exec()

And here is the legacy, PyQt5-style usage that also appeared to work:

noticeBox = QMessageBox(parent=self)
noticeBox.setText('Whoops - Upload destination not set.\n' +
                  'Save animation file to implicitly set or\n' +
                  'Edit metadata to explicitly set.')
noticeBox.setStandardButtons(QMessageBox.Ok)
noticeBox.setIcon(QMessageBox.Information)
resultFlag = noticeBox.exec_()

Additionally, there is a top-of-file import block that selects PyQt5 first, then falls back to PyQt6, and exits if neither is available:

# Pick local Qt binding: try PyQt5, else PyQt6, else abort
qt_binding = None
try:
    from PyQt5.QtCore import *
    from PyQt5.QtGui import *
    from PyQt5.QtWidgets import *
    from PyQt5 import QtMultimedia as media
    qt_binding = 5
except:
    try:
        from PyQt6.QtCore import *
        from PyQt6.QtGui import *
        from PyQt6.QtWidgets import *
        from PyQt6 import QtMultimedia as media
        qt_binding = 6
    except:
        sys.stderr.write('WHOOPS - Unable to find PyQt5 or PyQt6 - Quitting\n')
        exit(10)

What’s actually happening

The legacy code works because another dependency, qwt, brings in the QtPy abstraction layer under the hood. QtPy, and similar compatibility shims, exist to bridge API differences between bindings and Qt versions. One of the biggest changes in PyQt6 is that every Qt enum must live in its own namespace. This improves how Python sees enums (they’re now real Python enums) and how that aligns with Qt, but it also means that the old flat attribute access like QMessageBox.Ok is no longer available by default.

To smooth over those differences, a layer like QtPy programmatically recreates or attaches attributes on modules and classes at import time. Once a module is imported in Python, it stays in memory, and any attributes that the compatibility layer writes onto it become visible globally in that process. If QtPy adds an Ok attribute to QMessageBox, then QMessageBox.Ok appears to exist everywhere in your program. That explains the discrepancy: the app that imports qwt (which depends on QtPy) “inherits” the old enum access pattern; the app that doesn’t import it fails unless the new enum syntax is used.

By default, PyQt6 will raise an AttributeError if you attempt to access QMessageBox.Ok instead of QMessageBox.StandardButton.Ok. There is no built-in fallback. The apparent success only happens because the abstraction layer patched the module’s attributes. Some bindings behave differently: PySide6 supports the new Python enum approach but also includes an internal “forgiveness” mechanism, so QMessageBox.Ok still works there. That convenience might go away in the future, though.

Note that creating attributes on external objects is not always a great idea. Qt tries to keep names unique, but given the scale of the API, there’s no absolute guarantee. There are also performance trade-offs: these layers either patch a lot upfront—slowing startup—or patch on demand, which adds lookup overhead and introduces more moving parts possible to break in edge cases. Still, they can greatly reduce friction for codebases that need to straddle multiple Qt versions or bindings.

Solution: use Qt6 enum namespaces explicitly

The robust fix is to always use the fully qualified enum names required by Qt6. That avoids hidden coupling to a compatibility shim and ensures your code behaves the same whether or not a third-party abstraction layer is present.

Here is the corrected version of the legacy snippet using the Qt6 approach:

dialog = QMessageBox(parent=self)
dialog.setText('Whoops - Upload destination not set.\n' +
               'Save animation file to implicitly set or\n' +
               'Edit metadata to explicitly set.')
dialog.setStandardButtons(QMessageBox.StandardButton.Ok)
dialog.setIcon(QMessageBox.Icon.Information)
choice = dialog.exec()

If your project must run with older bindings too, a compatibility layer can still be useful, but it’s better to write the application code with the new enum namespaces and let the shim handle only what’s truly necessary. That way you won’t depend on side effects like patched attributes to make your dialogs work.

Why this matters

Understanding this behavior prevents accidental reliance on implicit patching. It also protects your code from environment-sensitive bugs. If one module quietly rewrites attributes on a Qt class, behavior can change depending on the import order, the presence of certain packages, or a minor refactor that removes a seemingly unrelated import. That’s not the kind of fragility you want in production.

There’s another operational angle: compatibility layers that introspect Qt modules and attach thousands of enums can increase startup time or create subtle regressions. If you need fast cold-start performance, think carefully about whether you want that patching going on in the background. If readability and a unified API across different environments are more important, the trade-off may be worth it—as long as you know what you’re signing up for.

Practical takeaways

Don’t expect PyQt6 to accept PyQt5-style enums. If you see legacy enum access working in a PyQt6-only environment, look for an abstraction layer like QtPy entering the process indirectly via another dependency such as qwt. Prefer the full enum namespaces in Qt6 code to avoid surprises. If you do use a compatibility layer, recognize the performance and maintainability implications of programmatically patched attributes. And in import fallbacks like the one above, consider using more specific exception handling in your project’s own code to make debugging simpler and safer, even if the behavior of the fallback stays the same.

In short, the safest path forward is to update your QMessageBox calls and other enums to the Qt6 namespace form and treat any compatibility shims as optional helpers, not hidden foundations.