2

I connected three buttons to a function which takes an argument and simply prints it. The buttons pass the argument in cosmetically different ways, as you can see in the MWE below, though as I far as I know there isn't any semantic difference.

from PyQt5.QtWidgets import QPushButton, QFrame, QApplication, QVBoxLayout
from functools import partial


def func(i):
    print(i)

app = QApplication([])
frame = QFrame()
layout = QVBoxLayout()
frame.setLayout(layout)

b1 = QPushButton('button 1', frame)
b1.clicked.connect(lambda: func(1))

b2 = QPushButton('button 2', frame)
b2.clicked.connect(partial(func, i=2))

b3 = QPushButton('button 3', frame)
b3.clicked.connect(lambda x=3: func(x))

layout.addWidget(b1)
layout.addWidget(b2)
layout.addWidget(b3)

frame.show()
app.exec()

So when I click the buttons 1 through 3 I expect to see

1
2
3

But instead I get

1
2
False

What's wrong with the last lambda? Could this possibly be a bug of PyQt?

PyQt version is 5.15.2.

gil
  • 2,086
  • 12
  • 13

2 Answers2

3

I believe this is because PyQt is passing a parameter to the connect function. From the documentation for QAbstractButton, from which QPushButton inherits, the clicked slot (callbacks for signals in PyQt are called slots) will receive a checked parameter, which is a boolean. The lambda function lambda x=3: func(x) takes one parameter, x, which takes default value 3. If no parameter is passed to this slot, x will therefore take the value 3. However, PyQt passes False to this function, as specified in the clicked signal documentation, so x instead takes the value False.

ouroboring
  • 581
  • 2
  • 14
  • I appreciate the quick reply. Maxime Chéramy's answer was even quicker though (and identical to yours if we count the comments), so I accepted his. – gil Dec 21 '20 at 17:08
  • I've done a +1 to this answer because I know it's always appreciated :). – Maxime Chéramy Dec 21 '20 at 17:25
1

You create a lambda with a default value:

lambda x=3: func(x)

But Qt passes False to it (the checked parameter), so the default argument isn't used.

Maxime Chéramy
  • 17,761
  • 8
  • 54
  • 75
  • 1
    Why doesn't the `lambda: func(1)` fail then? Does Qt check whether the function takes an argument? – Kelly Bundy Dec 21 '20 at 16:47
  • In that case, the lambda doesn't use the argument and call func with the value `1`. This is different than using a default value. – Maxime Chéramy Dec 21 '20 at 16:48
  • 1
    What do you mean it doesn't use it? Why wouldn't it fail like `(lambda: func(1))(False)` does, with `TypeError: () takes 0 positional arguments but 1 was given`? – Kelly Bundy Dec 21 '20 at 16:49
  • I believe Qt doesn't pass the argument if the callback doesn't take an argument in order to make the framework easier to use. – Maxime Chéramy Dec 21 '20 at 16:52
  • PyQt always calls a slot no matter if it has less arguments than what the signal provides. If the signal has two arguments and the function only takes one, the function is called anyway with the first signal argument. If the function doesn't take arguments, it's called anyway when the signal is emitted, even if it has arguments. – musicamante Dec 21 '20 at 16:58
  • Would you mind explaining (or pointing me to an existing explanation) what `checked` does and why would Qt pass it? I don't feel like overriding the default argument is making anything easier. – gil Dec 21 '20 at 17:01
  • @gil QPushButton, like all classes that inherit from QAbstractButton, has a `checked` state property that is used when the button is *checkable*. If the button is not checkable (the default for QPushButton), that value is always false. If the button is checkable, `clicked` behaves almost the same as the `toggled` signal, the difference is that `clicked` is only emitted when the user clicks the button, while `toggled` is *also* emitted when the button is toggled programmatically, using `setChecked()`. – musicamante Dec 21 '20 at 17:04
  • I see. Thank you. – gil Dec 21 '20 at 17:05
  • If you need to use a lambda, the solution is to add the actual parameter as a keyword, so that the first argument received from the signal can be ignored and the parameter you're interested to is correctly passed, also ensuring that the *scope* is respected: `lambda _, i=i: func(i)`. This is *very* important especially if you create connections in a for loop. – musicamante Dec 21 '20 at 17:07