Cannot set text on dynamically created QLabel-s when user clicks a button to display the chosen option from a QComboBox
-
I am new to GUI programming and I was creating a project for a course I am attending. I had to implement a feature where the user can pick an option from a drop-down and confirm it by clicking on a button which displays it on a QLabel below. However, when I choose an option and try to display it on the QLabel, it only displays the selected option from the last card's drop down on its QLabel. I have provided a reduced version of what it is supposed to be in my app below:
from PySide6 import QtWidgets import sys app = QtWidgets.QApplication([]) main_window = QtWidgets.QWidget() main_window_layout = QtWidgets.QVBoxLayout() main_window.setLayout(main_window_layout) cards_container_layout = QtWidgets.QHBoxLayout() main_window_layout.addLayout(cards_container_layout) for _ in range(3): card = QtWidgets.QWidget() card_layout = QtWidgets.QVBoxLayout() card.setLayout(card_layout) option_container_layout = QtWidgets.QHBoxLayout() card_layout.addLayout(option_container_layout) options = QtWidgets.QComboBox() options.setPlaceholderText("Choose an option") options.addItem("foo") options.addItem("bar") option_container_layout.addWidget(options) confirm_cur_option = QtWidgets.QPushButton("Add") option_container_layout.addWidget(confirm_cur_option) display_cur_option = QtWidgets.QLabel() card_layout.addWidget(display_cur_option) confirm_cur_option.clicked.connect(lambda : display_cur_option.setText(options.currentText())) confirm_cur_option.clicked.connect(lambda : print("clicked")) cards_container_layout.addWidget(card) main_window.show() sys.exit(app.exec())Result:

All buttons are emitting the clicked signal but it somehow only works on the last card's QLabel as you can see.
PySide version is 6.9.0 installed via
pip install pyside6==6.9.0in a virtual environment. -
I am new to GUI programming and I was creating a project for a course I am attending. I had to implement a feature where the user can pick an option from a drop-down and confirm it by clicking on a button which displays it on a QLabel below. However, when I choose an option and try to display it on the QLabel, it only displays the selected option from the last card's drop down on its QLabel. I have provided a reduced version of what it is supposed to be in my app below:
from PySide6 import QtWidgets import sys app = QtWidgets.QApplication([]) main_window = QtWidgets.QWidget() main_window_layout = QtWidgets.QVBoxLayout() main_window.setLayout(main_window_layout) cards_container_layout = QtWidgets.QHBoxLayout() main_window_layout.addLayout(cards_container_layout) for _ in range(3): card = QtWidgets.QWidget() card_layout = QtWidgets.QVBoxLayout() card.setLayout(card_layout) option_container_layout = QtWidgets.QHBoxLayout() card_layout.addLayout(option_container_layout) options = QtWidgets.QComboBox() options.setPlaceholderText("Choose an option") options.addItem("foo") options.addItem("bar") option_container_layout.addWidget(options) confirm_cur_option = QtWidgets.QPushButton("Add") option_container_layout.addWidget(confirm_cur_option) display_cur_option = QtWidgets.QLabel() card_layout.addWidget(display_cur_option) confirm_cur_option.clicked.connect(lambda : display_cur_option.setText(options.currentText())) confirm_cur_option.clicked.connect(lambda : print("clicked")) cards_container_layout.addWidget(card) main_window.show() sys.exit(app.exec())Result:

All buttons are emitting the clicked signal but it somehow only works on the last card's QLabel as you can see.
PySide version is 6.9.0 installed via
pip install pyside6==6.9.0in a virtual environment.@dynaspinner
This is tricky/non-intuitive when you use Python and lambdas!There are several ways to skin a cat, and here we might do it a different way if we didn't want to use a lambda. But let's say we do. Then the problem is how/when the statement
confirm_cur_option.clicked.connect(lambda : display_cur_option.setText(options.currentText()))gets interpreted. Here the
confirm_cur_optionis treated the way you would expect --- as the line is hit it takes the current value ofconfirm_cur_optionwhich is the latestQPushButtonjust created in the loop, so each one in turn, as you would expect. So far so good.But being in a lambda the
display_cur_optionandoptionsvariables do not get executed as you would/might expect. It is not interpreted as the values at the time theconnect()is executed. Rather its evaluation delayed. The values are taken only when the lambda is being executed at the instant the signal calls the lambda slot because of the connection. That means the values of bothdisplay_cur_optionandoptionswill always be the last value they had when your loop executed, and hence they are always just the finalQLabelandQPushButton.You have to do some Python lambda tomfoolery to achieve what you intend as follows:
confirm_cur_option.clicked.connect(lambda cur_opt=display_cur_option, opt=options : cur_opt.setText(opt.currentText()))You can use whatever you like for the "new" variable names to the left of the
=symbol, they are like formal parameters, some people even give them identical names to the actual parameter being passed in. It doesn't matter, I have chosen different names to make it clearer.This (somehow! it makes it more like a function call with parameters passed in from caller instead of local variables in the outside world which happen to be sitting around for use) makes Python do what you intend, so the values of the variables inside the lambda body will be captured from what they were at the time the
connect()was executed, not just local variables outside the lambda which have the value they happen to have been set to the last time they were changed.When I first did lambdas in Python (I'm more a C++-er) I was totally confused by this behaviour, and so have others been, so you are not alone facing this issue!
-
@dynaspinner
This is tricky/non-intuitive when you use Python and lambdas!There are several ways to skin a cat, and here we might do it a different way if we didn't want to use a lambda. But let's say we do. Then the problem is how/when the statement
confirm_cur_option.clicked.connect(lambda : display_cur_option.setText(options.currentText()))gets interpreted. Here the
confirm_cur_optionis treated the way you would expect --- as the line is hit it takes the current value ofconfirm_cur_optionwhich is the latestQPushButtonjust created in the loop, so each one in turn, as you would expect. So far so good.But being in a lambda the
display_cur_optionandoptionsvariables do not get executed as you would/might expect. It is not interpreted as the values at the time theconnect()is executed. Rather its evaluation delayed. The values are taken only when the lambda is being executed at the instant the signal calls the lambda slot because of the connection. That means the values of bothdisplay_cur_optionandoptionswill always be the last value they had when your loop executed, and hence they are always just the finalQLabelandQPushButton.You have to do some Python lambda tomfoolery to achieve what you intend as follows:
confirm_cur_option.clicked.connect(lambda cur_opt=display_cur_option, opt=options : cur_opt.setText(opt.currentText()))You can use whatever you like for the "new" variable names to the left of the
=symbol, they are like formal parameters, some people even give them identical names to the actual parameter being passed in. It doesn't matter, I have chosen different names to make it clearer.This (somehow! it makes it more like a function call with parameters passed in from caller instead of local variables in the outside world which happen to be sitting around for use) makes Python do what you intend, so the values of the variables inside the lambda body will be captured from what they were at the time the
connect()was executed, not just local variables outside the lambda which have the value they happen to have been set to the last time they were changed.When I first did lambdas in Python (I'm more a C++-er) I was totally confused by this behaviour, and so have others been, so you are not alone facing this issue!
@JonB Thanks for the response! I tried out the code but unfortunately I have a new problem now. For whatever reason, it thinks the first parameter is a bool type
Traceback (most recent call last): File "/home/dyna_spinner/PyProgramming/Projects/test.py", line 33, in <lambda> confirm_cur_option.clicked.connect(lambda cur_opt=display_cur_option, opt=options : cur_opt.setText(opt.currentText())) ^^^^^^^^^^^^^^^ AttributeError: 'bool' object has no attribute 'setText'Swapping the parameters' position around gives the same error saying the first parameter is a "bool" object.
On a side note about lambdas, I faced another problem with it a while ago where I called
self.parentWidget().layout().setCurrentIndex(0)to return to the first item in the stack layout which said "NoneType has no attribute layout()" but moving it to a function fixed the error. It only returned the error when instantiating the object. I think it was due to evaluatingself.parentWidget().layout().setCurrentIndex(0)during instantiating but I am not sure if that's the reason. -
@JonB Thanks for the response! I tried out the code but unfortunately I have a new problem now. For whatever reason, it thinks the first parameter is a bool type
Traceback (most recent call last): File "/home/dyna_spinner/PyProgramming/Projects/test.py", line 33, in <lambda> confirm_cur_option.clicked.connect(lambda cur_opt=display_cur_option, opt=options : cur_opt.setText(opt.currentText())) ^^^^^^^^^^^^^^^ AttributeError: 'bool' object has no attribute 'setText'Swapping the parameters' position around gives the same error saying the first parameter is a "bool" object.
On a side note about lambdas, I faced another problem with it a while ago where I called
self.parentWidget().layout().setCurrentIndex(0)to return to the first item in the stack layout which said "NoneType has no attribute layout()" but moving it to a function fixed the error. It only returned the error when instantiating the object. I think it was due to evaluatingself.parentWidget().layout().setCurrentIndex(0)during instantiating but I am not sure if that's the reason.@dynaspinner I printed the values and the first parameter always gives "False". It somehow converts it to a boolean but I don't know why.
-
@dynaspinner
This is tricky/non-intuitive when you use Python and lambdas!There are several ways to skin a cat, and here we might do it a different way if we didn't want to use a lambda. But let's say we do. Then the problem is how/when the statement
confirm_cur_option.clicked.connect(lambda : display_cur_option.setText(options.currentText()))gets interpreted. Here the
confirm_cur_optionis treated the way you would expect --- as the line is hit it takes the current value ofconfirm_cur_optionwhich is the latestQPushButtonjust created in the loop, so each one in turn, as you would expect. So far so good.But being in a lambda the
display_cur_optionandoptionsvariables do not get executed as you would/might expect. It is not interpreted as the values at the time theconnect()is executed. Rather its evaluation delayed. The values are taken only when the lambda is being executed at the instant the signal calls the lambda slot because of the connection. That means the values of bothdisplay_cur_optionandoptionswill always be the last value they had when your loop executed, and hence they are always just the finalQLabelandQPushButton.You have to do some Python lambda tomfoolery to achieve what you intend as follows:
confirm_cur_option.clicked.connect(lambda cur_opt=display_cur_option, opt=options : cur_opt.setText(opt.currentText()))You can use whatever you like for the "new" variable names to the left of the
=symbol, they are like formal parameters, some people even give them identical names to the actual parameter being passed in. It doesn't matter, I have chosen different names to make it clearer.This (somehow! it makes it more like a function call with parameters passed in from caller instead of local variables in the outside world which happen to be sitting around for use) makes Python do what you intend, so the values of the variables inside the lambda body will be captured from what they were at the time the
connect()was executed, not just local variables outside the lambda which have the value they happen to have been set to the last time they were changed.When I first did lambdas in Python (I'm more a C++-er) I was totally confused by this behaviour, and so have others been, so you are not alone facing this issue!
@JonB While searching for any answers to the error I posted when using your code, I stumbled upon this article which explains the quirk of
lambdain Python which confirms what you said:https://realpython.com/python-lambda/#evaluation-time
Posting this here in case someone stumbles upon this thread since I found it helpful.
-
@JonB Thanks for the response! I tried out the code but unfortunately I have a new problem now. For whatever reason, it thinks the first parameter is a bool type
Traceback (most recent call last): File "/home/dyna_spinner/PyProgramming/Projects/test.py", line 33, in <lambda> confirm_cur_option.clicked.connect(lambda cur_opt=display_cur_option, opt=options : cur_opt.setText(opt.currentText())) ^^^^^^^^^^^^^^^ AttributeError: 'bool' object has no attribute 'setText'Swapping the parameters' position around gives the same error saying the first parameter is a "bool" object.
On a side note about lambdas, I faced another problem with it a while ago where I called
self.parentWidget().layout().setCurrentIndex(0)to return to the first item in the stack layout which said "NoneType has no attribute layout()" but moving it to a function fixed the error. It only returned the error when instantiating the object. I think it was due to evaluatingself.parentWidget().layout().setCurrentIndex(0)during instantiating but I am not sure if that's the reason.@dynaspinner I figured out the reason. The first parameter gets overwritten by the
checkedvariable fromclickedand its default value is False. I made the following change and it works perfectly now:confirm_cur_option.clicked.connect(lambda _, cur_opt=display_cur_option, opt=options: cur_opt.setText(opt.currentText()))Thanks for the help @JonB !
-
D dynaspinner has marked this topic as solved on
-
D dynaspinner has marked this topic as solved on
-
@dynaspinner I figured out the reason. The first parameter gets overwritten by the
checkedvariable fromclickedand its default value is False. I made the following change and it works perfectly now:confirm_cur_option.clicked.connect(lambda _, cur_opt=display_cur_option, opt=options: cur_opt.setText(opt.currentText()))Thanks for the help @JonB !
@dynaspinner
I am not a regular Pythoneer. This does ring a bell, I think, as a further implementation detail/annoyance. Normally you can connect a slot to a signal which sends actual parameters and not bother to declare formal parameters in the slot (function or lambda) to receive them if you are not going to use them. This is same from C++. But if you want pass in your own extra parameters to a Python lambda, as in this case, I guess you are required then to have an explicit parameter (which must come before the extra ones) to receive the signal argument(s). I think I knew/discovered this at some point but had forgotten about this detail and the "checked" argument sent by theclicked()signal. -
Although the poster does not explain in "nice words", the stackoverflow answer at https://stackoverflow.com/a/35821092/489865 is exactly this/your situation, with the
clicked()signal.Ah, here in this forum back in 2021 it looks like I discovered this in the whole thread transmitting extra data with signals that already sends extra data :)
-
Although the poster does not explain in "nice words", the stackoverflow answer at https://stackoverflow.com/a/35821092/489865 is exactly this/your situation, with the
clicked()signal.Ah, here in this forum back in 2021 it looks like I discovered this in the whole thread transmitting extra data with signals that already sends extra data :)
@JonB I actually found out the solution to the error from the first link you provided. I'll check the other thread out right away. Thanks for the help and additional resources!
-
@JonB I actually found out the solution to the error from the first link you provided. I'll check the other thread out right away. Thanks for the help and additional resources!
@dynaspinner I have a lot of "vague memories" about discussions here :) Looks like I went through your pain to discover this complication at the time of the thread...!
-
@JonB While searching for any answers to the error I posted when using your code, I stumbled upon this article which explains the quirk of
lambdain Python which confirms what you said:https://realpython.com/python-lambda/#evaluation-time
Posting this here in case someone stumbles upon this thread since I found it helpful.
@dynaspinner I didn't realise the link I sent required you to sign in to check the article. I found a link from the official Python docs instead:
-
@dynaspinner I didn't realise the link I sent required you to sign in to check the article. I found a link from the official Python docs instead:
@dynaspinner ...so all in all that's why it's easier and clearer to use C++... ;)
-
@dynaspinner ...so all in all that's why it's easier and clearer to use C++... ;)
@JonB Haha yeah I might as well end up learning C++ for using Qt