Type stubs for PySide6
The most accurate type stubs for PySide! They have been tested using mypy on a code base with many thousands of lines of PySide code.
Features
- Type-safe signals: supports both custom and native signals. A
mypyplugin works around edge cases not currently supported by the Python type system (mapping/transformingTypeVarTuple). - Attention to detail: supports Qt subtleties such as passing property values to
__init__and implicitly convertible types. - Battle-tested: used in complex production code and backed by a test framework that confirm runtime and static equivalence.
Typed signals
types-PySide6 provides type safe signals and the attributes on native classes are populated
In our stubs, Signal and SignalInstance are generic types, parametrized by one or more signatures, where
each signature is a tuple of argument types: e.g.
Signal(int, str)is automatically detected as typeSignal[tuple[int, str]]Signal[tuple[int, str]]expects a slot function likedef myslot(arg1: int, arg2: str)- Signals with multipel signatures are also supported, e.g.
Signal((int, int), (str, str))produces the typeSignal[tuple[int, int], tuple[str, str]], and can work with a slot function likedef myslot(arg1: int, arg2: str)ordef myslot(arg1: str, arg2: str)
This provides type safety in a few ways:
SignalInstance.connect()enforces that the connected callable is compatible with the arguments emitted by the signal.- Signals can be connected to other signals, and the receiving signal's arguments are checked the same way as a slot's.
SignalInstance.emit()enforces the number and types of the arguments provided.- Signals with multiple signatures are checked against their default (first) signature, which is
the only signature an unsubscripted
connect()/emit()uses at runtime. Indexing, e.g.mysignal[str, str].connect(...), can be used to check against a specific signature: the index is validated against the signal's first or last signature (with up to four arguments each); the mypy plugin extends this to every declared signature. - A native signal with a defaulted C++ parameter, e.g.
void clicked(bool checked = false), is also declared with multiple signatures, because that is how Qt registers it (Signal[tuple[()], tuple[bool]]). There the trailing arguments genuinely are optional; the mypy plugin recognizes this case (see below).
The types of custom signals are inferred from the arguments passed to the Signal constructor in
common cases:
class MyObject(QtCore.QObject):
signal1 = QtCore.Signal() # Signal[tuple[()]]
signal2 = QtCore.Signal(int) # Signal[tuple[int]]
signal3 = QtCore.Signal(int, str) # Signal[tuple[int, str]]
signal4 = QtCore.Signal((int,), (str,)) # Signal[tuple[int], tuple[str]]
signal5 = QtCore.Signal((int, int), (str, str)) # Signal[tuple[int, int], tuple[str, str]]
Inference is supported for single-signature signals with up to four arguments, and for multi-signature signals with up to two signatures of up to two arguments each. Other configurations must be annotated manually.
Note that the actual Signal and SignalInstance classes are not subscriptable at runtime, so manual annotations must be forward references (wrapped in quotes):
signal6: "QtCore.Signal[tuple[int, str, float, bool, bytes]]" = QtCore.Signal(
int, str, float, bool, bytes
)
The mypy plugin
The stubs ship with an optional mypy plugin that improves signal type checking in ways that cannot be expressed in the stubs themselves.
To set it up, install the stubs and add the plugin (distributed inside the
types-PySide6 package) to your mypy
configuration:
# pyproject.toml
[tool.mypy]
plugins = ["types_pyside6_mypy_plugin"]
or in ini style:
# mypy.ini / setup.cfg
[mypy]
plugins = types_pyside6_mypy_plugin
Signal subscripts are validated against every signature
At runtime, subscripting a signal selects one of its declared signatures -- raising
IndexError when none matches -- and connect()/emit() on the result use exactly that
signature. (An unsubscripted connect()/emit() uses only the default signature: PySide
does not dispatch across genuinely distinct signatures by argument type, so the stubs'
default-signature checking is already what happens at runtime. Signatures that differ only
in the number of arguments are a different matter -- see the next section.)
The stubs alone can only validate an index against the signal's first or last signature:
an index selecting a middle signature of a three-plus-signature signal is falsely flagged,
and an index that matches no declared signature at all falls through to an unchecked
catch-all, hiding the runtime IndexError and leaving everything called on the result
unchecked. The plugin validates a literal index against every declared signature and
narrows the result to the signature the index selects, so subsequent connect()/emit()
calls are checked against it -- exactly mirroring the runtime dispatch:
class MyObject(QtCore.QObject):
signal: "QtCore.Signal[tuple[int, int], tuple[str], tuple[float, float]]" = (
QtCore.Signal((int, int), (str,), (float, float))
)
def use_signatures(self) -> None:
self.signal[str].emit("one") # stubs alone flag the valid index [str]
self.signal[str].emit("one", 2) # only the plugin catches this TypeError
self.signal[str].connect(int_slot) # only the plugin catches this bad slot
self.signal[str, str] # only the plugin catches this IndexError
self.signal[bool, bool] # ... and this one (bool is not int at runtime)
Signatures with C++ default arguments are checked correctly
Qt registers a separate signature for each parameter of a C++ signal with a default, so Qt's void clicked(bool checked = false) becomes PySide' Signal[tuple[()], tuple[bool]] -- the same as if the
Python signal were declared with two distinct signatures. The two do not behave the same
way, though: for a default argument C++ simply fills in the default.
Wherever one signature of a native signal is a prefix of another, the plugin ensures that
runtime behavior is reflected in the static check:
connect()accepts a slot taking as many arguments as the longest signature, because PySide connects a slot to the registered signature that has as many arguments as the slot. Sobutton.clicked.connect(self.on_click)type checks for anon_click(self, checked: bool), andcheckedreally is delivered. This is the case that madeclicked/triggered/destroyedslots hard to type.emit()may leave the defaulted arguments out:model.dataChanged.emit(topLeft, bottomRight)type checks, and slots taking the third argument still receive it, filled in by C++.- What
emit()may not do is pass more arguments than the default signature declares:button.clicked.emit(True)raisesTypeErrorat runtime, becauseclicked()is the default signature. Usebutton.clicked[bool].emit(True)to emit the other signature.
Signals declared in python are deliberately left strict: a signal with a similar prefix-like
relationship between the signatures -- e.g. Signal((int, str), (int,)) -- does not
make anything optional -- emit(1) raises TypeError at runtime, and thus the mypy plugin
enforces this statically.
Note: The plugin recognizes a native signal by where it is declared, which it can only see when the signal is used directly
(obj.sig.emit(...), self.sig.connect(...)); a signal read into a variable first
(sig = button.clicked) is checked strictly as if it were not a native C++ signal.
Use of object in signal instantiation can be configured to mean typing.Any
Signal(object) is the idiomatic way to declare a signal that emits an arbitrary value (Qt
registers it as PyObject).
class MyObject(QtCore.QObject):
signal1 = QtCore.Signal(object) # signal takes MyCustomClass
Since an argument typed as object accepts only values of type object or Any this
common pattern will lead to numerous errors in a typical codebase.
The proper way to handle this is to add a type annotation:
class MyObject(QtCore.QObject):
signal1: "Signal[tuple[MyCustomClass]]" = QtCore.Signal(object)
However, adding annotations throughout a codebase may be a heavy lift that you'd like to
defer till later, so the plugin defaults to loosening this check by internally overriding object
arguments in Signal(...) declarations to typing.Any:
Signal(object)becomesSignal[tuple[Any]]: any single-argument slot can be connected to it, and any value can be emitted through it.- The override is per-argument, so mixed signals stay strict where they can be:
Signal(int, object)infers asSignal[tuple[int, Any]], and connecting a slot whose first argument is not compatible withintis still an error. - Arguments declared as
typing.Any(a real class at runtime since Python 3.11, which PySide6 likewise registers asPyObject) are treated the same way, soSignal(Any)also works.
Plugin options
Options are read from the same config file that mypy was invoked with, from a
[tool.types-pyside6-mypy] table in pyproject.toml, or a [types-pyside6-mypy] section
in ini-style files:
# pyproject.toml
[tool.types-pyside6-mypy]
object_as_any = false
# mypy.ini / setup.cfg
[types-pyside6-mypy]
object_as_any = False
object_as_any(defaulttrue): set tofalseto opt out of rewritingobject/typing.Anyarguments ofSignal(...)declarations totyping.Any.
Rule-based fixes
The types-PySide6 stub generator inspects the annotations extracted from the PySide6 library and automatically applies the following fixes:
- When instantiating subclasses of
QObjectit is possible to pass the values of properties and signals as**kwargsto__init__. The stubs have been fix to include these args on all relevant__init__methods. - Removed redundant overlapping overloads, so that satisfying mypy/liskov on subclassed methods is easier
- Corrected all arguments typed as
typing.Sequenceto betyping.Iterable. Tests so far have indicated that this is true as a general rule. - Added sub-types to
Iterableannotations, e.g.Iterable[str],Iterable[int], etc - Replaced
objectwithtyping.Anyin return types. e.g.:QSettings.value() -> AnyQModelIndex.internalPointer() -> AnyQPersistentModelIndex.internalPointer() -> Any
- Added support for overloads that mix static and instance methods.
mypydisallows this using traditional overloads, so this project achieves it by generating specialized decorator classes that hold each of the overloads.
Specific fixes
- Certain argument types implicitly accept alternative types for brevity. Below are the known fixes so far (Note that I've debated not including these, since one of the advantages of static typing is it gives you the confidence to be explicit rather than ambiguous. I could introduce a strict mode in the future that would disable these):
QKeySequence:strQColor:Qt.GlobalColorandintQBrush:QLinearGradientandQColor(and by extensionQt.GlobalColor)QCursor:Qt.CursorShapeQEasingCurve:QEasingCurve.Type
- Fixed
QTreeWidgetItemIterator.__iter__()to returnIterator[QTreeWidgetItemIterator] - Added missing
QDialog.exec()method - Fixed numerous methods which accept
None:QPainter.drawText(..., br)QPainter.drawPolygon(..., arg__2)QProgressDialog.setCancelButton(button)*.setModel(model)QLabel.setPixmap(arg__1)
- Fixed numerous arguments that accept
QModelIndexwhich were typed asint - Fixed return type for
QApplication.instance()andQGuiApplication.instance() - Fixed return type for
QObject.findChild()andQObject.findChildren() - Fixed support for initializing
QDatefromdatetime.date - Fixed support for initializing
QDateTimefromdatetime.datetime - Fixed
QByteArray.__iter__()to returnIterator[bytes] - Fixed support for
bytes(QByteArray(b'foo')) - Added support for all
QSizeandQSizeFoperations - Added support for all
QPolygonoperations - Fixed
QTextEdit.setFontWeight()to acceptQFont.Weight - Fixed return type for
qVersion() - Add
QSpacerItem.__init__/changeSizeoverloads that use alternate names:hData->hPolicy,vData->vPolicy - Fixed
QAction.menuto return optionalQMenuinstead ofQOjbect
License
As a derived work from PySide6, the stubs are delivered under the LGPL v2.1 . See file LICENSE for more details.
Installation
Install the latest stub packages from pypi:
$ pip install types-PySide6
This will add the PySide6-stubs and shiboken6-stubs packages into your site-packages directory.
Yes, the name of the pypi package is types-PySide6 but the python package it installs is PySide6-stubs.
It's confusing, but PEP 561 requires that the installed package name is of the form $PACKAGE-stubs, so all of us PySide stub developers are installing a package with the same name.
Note, you may need to uninstall other PySide6 stubs first:
$ pip uninstall PySide6-stubs
Help improve the stubs
If you notice incorrect or missing typing information (i.e. mypy reports errors even though your code is correct), please report it or make a PR to fix it.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distributions
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file types_pyside6-6.10.3.0-py2.py3-none-any.whl.
File metadata
- Download URL: types_pyside6-6.10.3.0-py2.py3-none-any.whl
- Upload date:
- Size: 610.0 kB
- Tags: Python 2, Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
uv/0.8.17
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
abb3360637b1980c0d8b456c9b3f3031e0d0dd146df03b677e17a577e9892b8e
|
|
| MD5 |
752e12b4bc72d70910ecdedf7c2715c6
|
|
| BLAKE2b-256 |
49a2d4df95667ad290249bfb45f8082c7e4d5b46b8f3f01aa94a7420b9974116
|