Search code examples
pythonparameterspyqtpysidesignals-slots

pyqt signal emit with object instance or None


I would like to have a pyqt signal, which can either emit with a python "object" subclass argument or None. For example, I can do this:

valueChanged = pyqtSignal([MyClass], ['QString'])

but not this:

valueChanged = pyqtSignal([MyClass], ['None'])
TypeError: C++ type 'None' is not supported as a pyqtSignal() type argument type

or this:

valueChanged = pyqtSignal([MyClass], [])
TypeError: signal valueChanged[MyObject] has 1 argument(s) but 0 provided

I also tried None without the quotes and the c++ equivalent "NULL". But neither seems to work. What is it I need to do to make this work?


Solution

  • TLDR; There's no real solution to this problem. PyQt treats None in a special way when defining signal overloads, so the simplest solution is to use object instead. For an explanation of exactly why this is, see below.


    There are a couple of reasons why what you tried didn't work.

    Firstly, the type argument of pyqtSignal accepts either a python type-object or a string giving the name of a C++ object (i.e. a Qt class). Strings can't be used to specify python types - and in any case, None is a value, not a type.

    Secondly, None is special-cased by PyQt to allow the default overload of the signal to be used without explicitly selecting the signature. So, even if type(None) were used when defining signal overloads, it still wouldn't solve the problem.

    (With hindsight, it appears that a better design choice would have been for PyQt to use an internal sentinel object for the default, rather than special-casing None. However, it's possibly too late to change that now. The behaviour remains unchanged in PyQt5 & PyQt6 [as of 13-03-2023]).

    Just to spell this all out more clearly, here's some example code:

    class MyClass: pass
    
    class X(QtCore.QObject):
        valueChanged = QtCore.pyqtSignal([MyClass], [type(None)])
    
    x = X()
    x.valueChanged.connect(
        lambda *a: print('valueChanged[]:', a))
    x.valueChanged[MyClass].connect(
        lambda *a: print('valueChanged[MyClass]:', a))
    x.valueChanged[type(None)].connect(
        lambda *a: print('valueChanged[type(None)]:', a))
    
    print('emit: valueChanged[]')
    x.valueChanged.emit(MyClass())
    print('\nemit: valueChanged[type(None)]')
    x.valueChanged[type(None)].emit(MyClass())
    print('\nemit: valueChanged[MyClass]')
    x.valueChanged[MyClass].emit(MyClass())
    

    Output:

    emit: valueChanged[]
    valueChanged[]: (<__main__.MyClass object at 0x7f37ab0db8e0>,)
    valueChanged[MyClass]: (<__main__.MyClass object at 0x7f37ab0db8e0>,)
    valueChanged[type(None)]: (<__main__.MyClass object at 0x7f37ab0db8e0>,)
    
    emit: valueChanged[type(None)]
    valueChanged[]: (<__main__.MyClass object at 0x7f37ab0db8e0>,)
    valueChanged[MyClass]: (<__main__.MyClass object at 0x7f37ab0db8e0>,)
    valueChanged[type(None)]: (<__main__.MyClass object at 0x7f37ab0db8e0>,)
    
    emit: valueChanged[MyClass]
    valueChanged[]: (<__main__.MyClass object at 0x7f37ab0db940>,)
    valueChanged[MyClass]: (<__main__.MyClass object at 0x7f37ab0db940>,)
    valueChanged[type(None)]: (<__main__.MyClass object at 0x7f37ab0db940>,)
    
    

    As you can see, when type(None) is used as an overload, it becomes impossible to select anything other than the default overload (i.e. the first one specified in the list of arguments). All three behave in exactly the same way, because they all select the same MyClass overload.

    This also means that attempting to emit None will fail, because that value can never match the type of the selected overload (which defaults to MyClass).

    A simple work-around would be to use object instead of type(None) - but this has the downside of allowing literally any value to be passed. So if preserving type is important, another possible solution might be to define your own singleton sentinel class to use instead of None.