Search code examples
pythonclassmethodspyqtpyside

Why do rebound methods of PyQt classes raise a TypeError


Making my code compatible with PyQt5/6 and PySide2/6, I wrote

if not hasattr(QtCore.QDateTime, 'toPython'):  # fix for PyQt5/6
    QtCore.QDateTime.toPython = QtCore.QDateTime.toPyDateTime

Run with PyQt5 or PyQt6, this resulted in

TypeError: toPyDateTime(self): first argument of unbound method must have type 'QDateTime'

when the function gets called:

QtCore.QDateTime.currentDateTime().toPython()

But if I change the call into

QtCore.QDateTime.toPython(QtCore.QDateTime.currentDateTime())

there is no error.

However, when I change the first piece of code to

if not hasattr(QtCore.QDateTime, 'toPython'):  # fix for PyQt5/6
    QtCore.QDateTime.toPython = lambda self: QtCore.QDateTime.toPyDateTime(self)

everything works fine whatever way I call toPython function. Why do I need the lambda expression here at all?

Added. To explain the behavior I'd expect, there is a piece of simple code:

class A:
    def __init__(self) -> None:
        print(f'make A ({hex(id(self))})')

    def foo(self) -> None:
        print(f'A ({hex(id(self))}) foo')


class B:
    def __init__(self) -> None:
        print(f'make B ({hex(id(self))})')


B.bar = A.foo

b: B = B()  # prints “make B (0x7efc04c67f10)” (or another id)
B.bar(b)    # prints “A (0x7efc04c67f10) foo” (same id, no error)
b.bar()     # same result as before, no error

On the contrary, the following code doesn't work:

from PyQt6.QtCore import QDateTime

QDateTime.toPython = QDateTime.toPyDateTime
t: QDateTime = QDateTime.currentDateTime()
QDateTime.toPython(t)  # no error
t.toPython()           # raises TypeError

Solution

  • This is due to an implementation difference between PyQt and PySide. In the former, most methods of classes are thin wrappers around C-functions which don't implement the descriptor protocol (i.e. they don't have a __get__ method). So, in this respect, they are equivalent to built-in functions, like len:

    >>> type(len)
    <class 'builtin_function_or_method'>
    >>> type(QtCore.QDateTime.toPyDateTime) is type(len)
    True
    >>> hasattr(QtCore.QDateTime.toPyDateTime, '__get__')
    False
    

    By contrast, most PySide methods do implement the descriptor protocol:

    >>> type(QtCore.QDateTime.toPython)
    <class 'method_descriptor'>
    >>> hasattr(QtCore.QDateTime.toPython, '__get__')
    True
    

    This means that if you reversed your compatibilty fix, it would work as expected:

    >>> from PySide2 import QtCore
    QtCore.QDateTime.toPyDateTime = QtCore.QDateTime.toPython
    >>> QtCore.QDateTime.currentDateTime().toPyDateTime()
    datetime.datetime(2022, 4, 29, 11, 52, 51, 67000)
    

    However, if you want to keep your current naming scheme, using a wrapper function (e.g. like lambda) is essentially the best you can do. All user-defined Python functions support the descriptor protocol, which is why your example using simple user-defined classes behaves as expected. The only improvement that might be suggested is to use partialmethod instead. This will save writing some boiler-plate code and has the additional benefit of providing more informative error-messages:

    >>> QtCore.QDateTime.toPython = partialmethod(QtCore.QDateTime.toPyDateTime)
    >>> d = QtCore.QDateTime.currentDateTime()
    >>> d.toPython()
    datetime.datetime(2022, 4, 29, 12, 13, 15, 434000)
    >>> d.toPython(42)
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "/usr/lib/python3.10/functools.py", line 388, in _method
        return self.func(cls_or_self, *self.args, *args, **keywords)
    TypeError: toPyDateTime(self): too many arguments
    

    I suppose the only remaining point here is the question of why exactly PyQt and PySide differ in their implementations. You'd probably have to ask the author of PyQt to a get a definitive explanation regarding this. My guess would be that it's at least partly for historical reasons, since PyQt has been around a lot longer than PySide - but there are no doubt several other technical considerations as well.