Suggested solutions for Tkinter loop + command not working as expected

I’m having issues using lambda functions to dynamically create commands for my Tkinter GUI, and the suggested solutions I’ve found on SO don’t seem to work as expected.

The objective of this simple code is to create a GUI that should have two buttons, one that says “Buy Apple” and one that says “Buy Banana”. When the user clicks the ‘apple’ button, it should print a message that says ‘You bought 1 apple’ and when they click the ‘banana’ button, it should print a message that says ‘You bought 1 banana’.

The original form was this:

import tkinter as tk
from functools import partial

root = tk.Tk()

def print_buy(fruit):
    print('You bought 1 ' + f)

for f in ['Apple', 'Banana']:
    btn = tk.Button(
        root,
        text = 'Buy ' + f,
        command = lambda: print_buy(f)
    )
    btn.pack()

root.mainloop()

Now, I understand this doesn’t work because the command will reference the CURRENT value of f when the button is clicked, which will be ‘banana’, since that’s the value at the end of the loop. There are two suggested solutions on Stack Overflow here. The problem is, neither one is working as expected.

Suggested Fix 1: Partials

One suggestion is to use partials to ‘freeze’ the value in place. Here’s the loop with the partials:

for f in ['Apple', 'Banana']:
    btn = tk.Button(
        root,
        text = 'Buy ' + f,
        command = partial(print_buy, f)
    )
    btn.pack()

Nope. Still get ‘You get 1 banana’ for both buttons.

Suggested Fix 2: Assignment

OK, trying the other method suggested–assigning a variable inside the lambda function.

for f in ['Apple', 'Banana']:
    btn = tk.Button(
        root,
        text = 'Buy ' + f,
        command = lambda f=f: print_buy(f)
    )
    btn.pack()

Nothing but bananas.

Experimental Fix: Variable Variation

OK, maybe reusing the btn variable is causing issues?

for f in ['Apple', 'Banana']:
    btn_var = 'btn_' + f.lower()
    locals()[btn_var] = tk.Button(
        root,
        text = 'Buy ' + f,
        command = lambda f=f: print_buy(f)
    )
    locals()[btn_var].pack()

Nope, more bananas.

I’m sure I’ve missed something very basic here but I’ve been troubleshooting this for an hour and for the life of me I can’t work out what the problem is.

Look closely at print_buy:

def print_buy(fruit):
    print('You bought 1 ' + f)

What is the name of the parameter? And what is the name of the variable that you’re printing out?

1 Like

@MRAB You’re right, that’s the problem.

I knew it was going to be something stupid (given that the same solutions were provided on multiple SO questions) but dang, it’s always so irritating when it’s something that should be obvious. I didn’t catch that even after taking my code out and and simplifying it down it to test these solutions in isolation.

Thank you for the help!

Just wanted to mention that a linter can be of help here. If I run
pylint on this code:

 def print_it(fruit):
   print("fruit =", f)

it says:

 W0613: Unused argument 'fruit' (unused-argument)

Linters are quite good for identifying common trivial problems.

They’re not always correct, but by either fixing what they find or
telling the linter that it’s wrong (sometimes it is) you can clear
trivia out leaving you freer to think around real problems.

Cheers,
Cameron Simpson cs@cskk.id.au

@cameron I wanted to come back and say THANK YOU on this. I hadn’t run across linters so starting to use one on all my code is really helping me find more best practices (since I’m self taught and not doing this professionally).

1 Like