Skip to main content

Qt for Python Release: 6.12 is here!

Comments

Qt for Python 6.12 is here, alongside the Qt 6.12 LTS release. This is the biggest release we have had in a while. If you haven't read what's new in Qt itself, check out the general announcement.

Three things stand out:

  • Reactive properties for QML: The new QtQmlFeatures module removes most of the signal-and-property boilerplate from your Python classes.
  • PySide6 on iOS: The long-awaited iOS support is here.
  • A security review of our tooling, motivated by the EU Cyber Resilience Act (CRA): We threat-modeled Qt for Python, fixed the issues we found, and added security markers to our source files.

Additionally, this release also contains an extensive list of fixes across Qt for Python.

QtQmlFeatures: reactive properties for Python (Technical Preview)

Exposing a Python value to QML takes more code than it should. A single `price` property needs a `Signal`, a backing field, a getter, and a setter that checks changes and emits the signal. Derived values need even more wiring on top of that.

The new QtQmlFeatures module removes most of it. Here is a shopping cart with `price`, `quantity`, and a derived `total`, written the traditional way:

class Cart(QObject):
    priceChanged = Signal()
    quantityChanged = Signal()
    totalChanged = Signal()

    def __init__(self, parent=None):
        super().__init__(parent)
        self._price = 10
        self._quantity = 2
        self._total = self._price * self._quantity
        self.priceChanged.connect(self._recompute_total)
        self.quantityChanged.connect(self._recompute_total)

     @Property(int, notify=priceChanged)
     def price(self):
         return self._price

     @price.setter
     def price(self, value):
         if self._price == value:
             return
         self._price = value
         self.priceChanged.emit()

     # ...the same again for quantity and total

 

And here is the same cart with the new decorators. It also warns on large price jumps and logs every change. The traditional version has to do that inside each setter.

@auto_properties
class Cart(QObject):
     def __init__(self, parent=None):
         super().__init__(parent)
         self.price = 10
         self.quantity = 2

     @computed("price", "quantity")
     def total(self) -> int:
         return self.price * self.quantity

     @watch("price")
     def on_price_changed(self, change: Change):
     if change.new > change.old * 1.5:
         print(f"warning: price jumped {change.old} -> {change.new}")

     @effect("price", "quantity")
     def log_state(self):
         print(f"cart: {self.price} x {self.quantity}")

 

The properties are still real Q_PROPERTYs, so QML can read and write them. When QML changes `price`, the watcher fires, `total` updates, and the bindings refresh. You write no signal connections.

Four decorators do the work:

  • `@auto_properties` turns plain `self.x = ...` assignments into properties with notify signals.
  • `@computed` declares a cached, read-only value that refreshes when its dependencies change.
  • `@watch` runs after a property changes and gives you the old and new values.
  • `@effect` runs whenever any of the listed properties change.

The Reactive Properties tutorial builds this cart step by step. It starts from the traditional version and removes one chunk of boilerplate per step. Additionally, the following examples are updated to use QtQmlFeatures:

Two new examples were also added:

  • QML component loading example: Demonstrates how to load a QML defined type from Python with load_qml_component
  • Quick controls from Python: Demonstrates how to load QtQuick Controls types from Python with load_qml_component and drive their functionality from Python

iOS support to boost our mobile compatibility

We mentioned in previous posts that iOS was in progress, and we're happy to say that support for iOS has been added (PYSIDE-2352). We would like to thank everyone who contributed along the way until reaching this milestone.

Getting started takes three steps:

  1. Get the iOS wheels.

    iOS needs its own PySide6 and shiboken6 wheels, you can download them from the Qt for Python downloads page, or fetch them with qtpip:

     

    $ pip install qtpip

    $ qtpip download PySide6 --ios --arch arm64 --platform device

    Use --platform simulator to get the wheels for the iOS Simulator instead. Each wheel targets one architecture and one platform.


  2. Generate the Xcode project.

    The new pyside6-ios-deploy tool takes your project and the two wheels and generates an Xcode project. It uses the same pysidedeploy.spec file as pyside6-deploy and pyside6-android-deploy.

      $ pyside6-ios-deploy /path/to/main.py --name "<AppName>" \

                                            --wheel-pyside=<PySide6 wheel> \

                                           --wheel-shiboken=<shiboken wheel>

     

    The tool needs a macOS host. It downloads Python.xcframework automatically on first use.

  3. Build and run on Xcode.

    Open the generated .xcodeproj, then run it on the simulator or on a connected device. Signing and provisioning stay in Xcode. For a device build, pass your Apple Developer Team ID with --team-id.

     

 

For the full story, see pyside6-ios-deploy documentation and read our dedicated post, Python Mobile App Development: Bringing PySide6 on iOS which has been updated to have the latest features and options.

Deployment: smoother, and on more platforms

Deployment saw many changes in this release.

pyside6-deploy

  • It can now be configured through a [tool.pyside6.deploy] section in pyproject.toml (PYSIDE-1612).
  • pyside6-deploy, pyside6-android-deploy, and pyside6-project deploy accept --no-install to skip automatic installation of build dependencies.
  • ⚠️ Behavior change: pyside6-deploy now creates standalone packages by default (PYSIDE-3459). Use --onefile to get the previous default.

Android

  • The NDK was updated to r28c, and the limited API wheel tag was raised to cp310.
  • Host Python versions 3.10 through 3.14 are supported, and the resulting binaries are compatible with devices using 16 KB memory pages (PYSIDE-3425).
  • The tool now reports unmet prerequisites instead of hanging, and reliably deploys applications using QtQmlFeatures.
  • The prebuilt CPython is now used, replacing manual cross-compilation (PYSIDE-3426).
  • We fixed the JDK lookup and the NDK quarantine on macOS.

Security hardening

Now that the Cyber Resilience Act (CRA) reporting obligations apply, security is getting more attention. Security has always been a priority for Qt for Python. For 6.12, we looked at our tools, the way an attacker would and did a complete threat analysis of Qt for Python.

Some of the notable changes here that have some effect on developer experience here are:

  • Android deployment: a potential zip slip during wheel extraction was fixed, and downloaded NDK and command-line tools are now verified before being extracted.
  • pyside6-qml: it now warns before importing Python files located next to the QML source.
  • pyside6-deploy: a potential shell injection on Windows was fixed.
  • Serialization: deserialization of non-trivial classes from QDataStream and QSettings is now blocked.
  • Qt Widgets Designer: it now asks for a one-time confirmation before loading Python plugins.
  • QUiLoader: loadUiType() no longer searches for the pyside6-uic binary in the path. This now requires an explicit opt-in through a keyword argument and triggers a warning. We generally recommend moving away from this function in favor of running pyside6-uic at build time.
  • Shiboken6: when generating doxygen documentation, XML is parsed with XML_PARSE_NO_XXE for libxml ≥ 2.13, and entity expansion is disabled for older versions.

⚠️ We will be adding SBOM documents in future releases, because they are not yet ready.

New Qt Web Engine and Qt PDF distribution

If you are a Qt Web Engine user, you probably already read the blog post where a separation of the releases was announced. For that reason, we are following this configuration in order to ensure developers will get the latest releases alongside PySide releases.

Because our wheels contains the binaries of the modules, the schema that we will be using is to have wheel packages with a version that combines Qt full version + chromium version, this means that for Qt WebEngine 6.140 we will have a release in 6.12 that will have the version: 6.12.0.140, which will enable us to release new wheels both for new bug-fix releases in Qt, and new chromium releases for WebEngine.

The natural dependencies of Qt PDF module is also affected for this new structure and will follow the same configuration.

Python: new versions and improvement

  • Python 3.15: initial support has been added (PYSIDE-3221). Shiboken generates the new module initialization code within conditionals for 3.15.
  • Type hints: QDate and QDateTime now accept Python datetime types (PYSIDE-3038), and the __copy__ return annotation was fixed (PYSIDE-3338).
  • New APIs: QBitArray.fromBits(PyBuffer) and QBitArray.bits() (PYSIDE-3335). QMetaObject::className() now returns qualified names for nested classes, such as Outer.Inner (PYSIDE-3294).
  • Diagnostics: a warning is now shown when an unknown signal parameter is encountered (PYSIDE-3364).
  • Free threaded Python: There is an on-going effort by Quansight to enable free-threaded support in PySide, that hopefully we will have news soon.

Stability fixes

  • A memory leak in QStandardItemModel when calling clear() after populating through invisibleRootItem() (PYSIDE-3365)
  • A crash when returning a class implementing the sequence protocol from QAbstractItemModel.data() (PYSIDE-3423)
  • A crash retrieving the wrapper for QtWebEngineCore.ResourceRequestBody (PYSIDE-3409)
  • A crash caused by stale QLayoutItem pointers due to incorrect parenting (PYSIDE-3469)
  • Connections to private methods (prefixed by __) invoked by derived classes (PYSIDE-3376)
  • CMake build regressions affecting header packaging and include paths (PYSIDE-3306)

⚠️ The copy constructors of QTreeWidgetItem, QTableWidgetItem, and QListWidgetItem have been removed (PYSIDE-3367).

Shiboken6 updates

  • A type system attribute for GIL usage in free-threaded builds (PYSIDE-2221)
  • Blocking functions, like QEventLoop.exec(), can now be flagged through a function modification (PYSIDE-3356)
  • Multiphase module initialization code is now also generated for the deployed case, after a related Nuitka bug was fixed (PYSIDE-3155)
  • The __module__ attribute of shiboken6's ObjectType/Object types was fixed (PYSIDE-3336)
  • shibokenmodule: a recursive version of dump() for inspecting parent/child relationships (PYSIDE-3469)

Getting Qt for Python

As always, you can get the latest version by running:

     pip install pyside6

If you use uv, add it to your project and sync the environment:

  uv add pyside6
  uv sync

or if you are a commercial user:

         qtpip install pyside6

Let's stay in touch!

We want to keep trying new experimental support, features, and Python module integration. What should we do next? Drop us a message or open a suggestion on the Qt Bug Tracker 👍.

We hope you enjoy the release. As always, drop by our community platforms and let us know if something is not working properly by opening a bug report.

 

Blog Topics

Comments

Subscribe to our blog

Try Qt 6.12 Now!

Download the latest release here: www.qt.io/download

Qt 6.12 LTS is now available, with new features and improvements for application developers and device creators.

We're Hiring

Check out all our open positions here and follow us on Instagram to see what it's like to be #QtPeople.