18. More Signals & Slots

18.1 A Common Pitfall

We have already seen that a Python lambda can be used to pass additional arguments to a slot. However, there is a pitfall that makes lambdas a bit tricky to use: a lambda captures a variable by reference, not by value. The name is looked up in the enclosing scope only when the lambda actually runs, not when it’s created.[^1]. Let’s say you have five lambda functions and you want to pass them a loop index as the parameter:

1 functions = []
2 
3 for i in range(5):
4     functions.append(lambda: print(i))
5 
6 print("Calling the functions after the loop:")
7 for func in functions:
8     func()

The output is:

1 Calling the functions after the loop:
2 4
3 4
4 4
5 4
6 4

All five lambdas share the same closure over i, and by the time any of them actually runs (in the second loop) the i variable value is 4 so all four print 4 instead of 0, 1, 2, 3 and 4 as one might expect. You can fix this by giving i to the lambda as a default argument. Default arguments are evaluated once, at the moment the lambda is defined, so each lamda gets its own frozen copy of i current value:

1 functions = []
2 
3 for i in range(5):
4     functions.append(lambda x=i: print(x))
5 
6 print("Calling the fixed functions:")
7 for func in functions:
8     func()

Now, the output is:

1 Calling the fixed functions:
2 0
3 1
4 2
5 3
6 4

You need to be aware of this when using Python lambdas in your PySide6 code, for example when you want to log a QCheckBox checked state changes into multiple log files:

In the example we create a checkbox and use a loop to connect its checkStateChanged() signal to the lambdas. In each loop iteration, we capture the index and checkbox checkState() current values. If you check the checkbox, the output is:

 1 Logging to file no: 0
 2 State: CheckState.Checked
 3 Logging to file no: 1
 4 State: CheckState.Checked
 5 Logging to file no: 2
 6 State: CheckState.Checked
 7 Logging to file no: 3
 8 State: CheckState.Checked
 9 Logging to file no: 4
10 State: CheckState.Checked

We connect each lambda with two parameters, but only the second one (x) has a default value:

1 lambda state, x=i:
2     self.log_to_file(state, x)

You might think that you need to supply default arguments for state as well, but you don’t. You need to give a default only to parameters beyond what the signal itself supplies. checkStateChanged always emits exactly one argument (state) every time it fires, so PySide6 fills that first parameter positionally on every call, no matter what default you give it.

There is one more detail to note: checkStateChanged passes a Qt.CheckState value as its argument and you need to import Qt from PySide6.QtCore for it to work even though Qt is never referenced directly in the code (see 5.3 for details).

18.2 Custom Signals

Most Qt classes provide a set of predefined signals. When you create your own QObject-derived class, you will often want it to emit signals of its own. Suppose you need a custom button class that keeps track of how many times it’s been clicked, and notifies other objects when the count changes.

The Signal class from PySide6.QtCore is used to declare custom signals. Its __init__() arguments are all optional:

1 __init__([type1 [, type2...]] [, name="" [, arguments=[]]])
  • [type1 [, type2...]] - the types of the arguments that the signal will emit.
  • name - the signal name. If omitted (the usual case), the signal takes the name of the attribute that it is assigned to.
  • arguments - a list of strings giving the argument names (rarely useful in Qt Widgets applications).

In the example the signal is declared as a class attribute on CounterButton:

1 counterChanged = Signal(int)

Because neither name nor arguments is supplied, the signal is named counterChanged, and carries a single unnamed int argument. PySide6 turns this class-level declaration into a distinct signal object for every CounterButton instance. You never create Signal instances yourself.

Signal provides three methods:

Method Signature / Parameters Description
connect connect(receiver[, type=Qt.AutoConnection]) Create a connection between this signal and a receiver.
disconnect disconnect(receiver) Disconnect this signal from the receiver.
emit emit(*args) Emits the signal.
  • receiver mau be a Python callable, a @Slot, or another Signal.
  • type is a Qt.ConnectionType value (covered in Chapter 32).

Inside CounterButton, the button’s own clicked() signal is connected to on_clicked(). The method increments self.counter and emits counterChanged with the new value. In the main window the custom signal is used exactly like any built-in signal:

1 self.button.counterChanged.connect(self.on_counter_changed)

The window also connects the button’s regular clicked() signal to its own slot. Each click produces output like:

1 Counter:  1
2 Button clicked
3 Counter:  2
4 Button clicked
5 Counter:  3
6 Button clicked

18.3 Signal Blocking

At times, you’ll want to prevent a signal from being emitted at all, for instance during class initialization, or when a programmatic change to a widget’s value would otherwise trigger unwanted side effects through a connected slot.

To temporarily block a signal, use the QObject.blockSignals() passing it a boolean value. If the value is True all signals emitted by the object are blocked. If the value is False, signals are not blocked. It is common to use this pattern to temporarily block an object’s signals:

1 my_qobject.blockSignals(True)
2 [sensitive operations]
3 my_qobject.blockSignals(False)

In the example we have a button’s clicked() signal connected to a slot named on_button_clicked(). We use a QCheckBox() to block/unblock the button’s signals:

1 if state == Qt.CheckState.Checked:
2     self.button.blockSignals(True)
3     print('Signals blocked!')
4 else:
5     self.button.blockSignals(False)
6     print('Signals unblocked!')

When the checkbox is checked the button’s signals are blocked:

1 Button clicked, checked: False
2 Signals blocked!
3 Signals unblocked!
4 Button clicked, checked: False

Note that the button itself keeps receiving and processing clicks - it still shows visual feedback, but emitting a signal will not invoke anything connected to it. blockSignals() also returns the object’s previous blocked state, which is useful if you need to restore it afterward. Blocking signals is used in several examples in this book, e.g. in section 10.1 to temporarily suppress QTreeWidget’s signals while it is being populated with items.

18.4 Connection Objects

The Signal.connect() method has a return value of type QMetaObject.Connection. It is a handle to the signal-slot connection the Signal.connection() call established and you can use it later to break that specific connection.

The return value of self.button.clicked.connect(self.on_button_clicked) is stored in self.conn. A second button uses that stored reference to disconnect:

1 self.button.clicked.disconnect(self.conn)

Before disconnectiong, the code checks whether the connection is still valid as Connection objects support boolean evaluation.

The output is:

1 <class 'PySide6.QtCore.QMetaObject.Connection'>
2 Connection is valid
3 <class 'PySide6.QtCore.QMetaObject.Connection'>
4 Connection is invalid
5 Already disconnected

The first time we click the Disconnect button the connection is valid and succesfully disconnected. The second time we click it the connection is not valid so we don’t call disconnect()

If we didn’t check the connection validity we would have got a warning:

1 RuntimeWarning: Failed to disconnect (<PySide6.QtCore.QMetaObject.Connection object at 0x7fee46154040>) from signal "clicked()".

18.5 Connecting Multiple Slots with a Signal

You can connect a slot to more than one signals. The example we create three slots and connect them with the same button’s clicked() signal.

Clicking the button produces:

1 Executed first
2 Executed second
3 Executed third

The slots run in the order in which they were connected to the signal. This not guaranteed for signal-slot connections across different threads.

18.6 Disconnecting

You can break a signal-slot connection at any time by calling using the Signal.disconnect() method, passing the connected slot:

1 self.button.clicked.disconnect(self.on_clicked)

This breaks up the connection between button.clicked signal and the on_clicked() slot. However, QObject also has a disconnect() method with several overloads that give you more control over which signal-slot connections are removed.

  • static disconnect(connection) lets you pass a connection object to it. In the example we store all connection objects in the Window.connections list. On clicking the Disconnect 1 button all connections are disconnected in a loop:
1 def on_disconnect_1(self):
2     for c in self.connections:
3         QObject.disconnect(c)
4     self.update_label()
  • static disconnect(sender, signal, receiver, member) lets you specify both the sender and the receiverobjects as well as thesignaland thememberie the slot. On clicking theDisconnect 2button we setself.buttonas thesenderand the other three arguments toNone. Noneacts as a wildcard so this disconnects **all** signal-slot connections whereself.button` is the signal sender.
1 def on_disconnect_2(self):
2     QObject.disconnect(self.button, None, None, None)
3     self.update_label()
  • On clicking the Disconnect 3 button we set self.button as sender and clicked(bool) as signal. receiverandmemberare still set toNone`.
1 def on_disconnect_3(self):
2     QObject.disconnect(self.button, SIGNAL('clicked(bool)'), None, None)
3     self.update_label()

This will disconnect only the slots with the signature clicked(bool) ie. Slot1 and Slot3. The connection between button.clicked and Slot2 remains.

  • On clicking the Disconnect 4 button we set sender to button and reciver to self (ie to the Window instance). signal and member are set to None. This breaks up all connections since all the slots are Window members.
1 def on_disconnect_4(self):
2     QObject.disconnect(self.button, None, self, None)
3     self.update_label()