64

I am trying to create buttons in tkinter within a for loop. And with each loop pass the i count value out as an argument in the command value. So when the function is called from the command value I can tell which button was pressed and act accordingly.

The problem is, say the length is 3, it will create 3 buttons with titles Game 1 through Game 3 but when any of the buttons are pressed the printed value is always 2, the last iteration. So it appears the buttons are being made as separate entities, but the i value in the command arguments seem to be all the same. Here is the code:

def createGameURLs(self):
    self.button = []
    for i in range(3):
        self.button.append(Button(self, text='Game '+str(i+1),
                                  command=lambda: self.open_this(i)))
        self.button[i].grid(column=4, row=i+1, sticky=W)

def open_this(self, myNum):
    print(myNum)

Is there a way to get the current i value, each iteration, to stick with that particular button?


This problem can be considered a special case of Creating functions in a loop. There's also What do lambda function closures capture?, for a more technical overview.

See also How to pass arguments to a Button command in Tkinter? for the general problem of passing arguments to Button callbacks.

Karl Knechtel
  • 62,466
  • 11
  • 102
  • 153
Marcel
  • 649
  • 1
  • 6
  • 3

4 Answers4

152

Change your lambda to lambda i=i: self.open_this(i).

This may look magical, but here's what's happening. When you use that lambda to define your function, the open_this call doesn't get the value of the variable i at the time you define the function. Instead, it makes a closure, which is sort of like a note to itself saying "I should look for what the value of the variable i is at the time that I am called". Of course, the function is called after the loop is over, so at that time i will always be equal to the last value from the loop.

Using the i=i trick causes your function to store the current value of i at the time your lambda is defined, instead of waiting to look up the value of i later.

BrenBarn
  • 242,874
  • 37
  • 412
  • 384
  • 2
    what if we wanted to pass two arguments to a function like open_this? – Amen Oct 14 '14 at 17:10
  • 11
    @Amen: It depends what you want those arguments to be. If both are coming from some external loop and you want to "freeze" them both in way shown above, you would just do `lambda x=x, y=y: self.open_this(x, y)`. – BrenBarn Oct 14 '14 at 18:06
  • 4
    This is brilliant, straightforward and good explanation. This should be the answer. – Battleroid Mar 01 '16 at 23:38
  • 1
    @BrenBarn Just now i came across same situation, its really amazing. I really weird how you all came across these kind of techniques. Thank you so much. – velpandian Mar 03 '18 at 16:59
  • at first this did not make sense. Kudos! – Lucem Mar 29 '20 at 09:12
  • Has that `lambda i=i: ....` syntax always been available in python? I never saw that before, but seeing how the answer is dated 8 years ago, I guess it's been around a while. I could have used that more than once in my travels! – RufusVS Jun 28 '20 at 19:43
  • 1
    @RufusVS: It's just default argument syntax. It's the same as when you do `def foo(x, y=3)`. It's just that here the name of the function argument is the same as the name of a variable in the enclosing scope. – BrenBarn Jun 28 '20 at 23:23
  • @BrenBarn it's funny, but I never made that connection with lambda functions, of just taking the parenthesized arguments of a regular function and place them before the colon of the lambda function! I'm sure I won't forget it now. – RufusVS Jun 29 '20 at 23:00
  • @RufusVS `lambda` functions are just anonymous functions. You can still use arguments/parameters with it, just like any function. See [Lambda Expressions](https://docs.python.org/3/tutorial/controlflow.html?highlight=lambda#lambda-expressions) for a little more information. – howdoicode Nov 29 '20 at 15:33
  • "_Using the i=i trick causes your function to store the current value_", I think of it more like it creates a new function over each loop whose arguments are stored in the newly created functions, dont know if this is correct :) – Delrius Euphoria Jan 20 '21 at 07:55
  • 1
    @CoolCloud: That is the same as what I said. You said "the arguments are stored in the functions". I said "the function stores the value". It's the same thing. – BrenBarn Jan 21 '21 at 08:08
  • @BrenBarn Thanks a lot. It worked perfectly and the explanation was great ;). Thank you so much. – Mohammad Sep 14 '21 at 20:50
  • It bothers me to no end that "deliberately exploit [one of the most famous gotchas in Python](https://stackoverflow.com/questions/1132941/)" is the popular solution for this while "use a standard library tool specifically designed for the purpose of binding parameters" (i.e., `functools.partial`) is practically forgotten. – Karl Knechtel May 11 '22 at 11:40
12

This is how closures work in python. I ran into this problem myself once. You could use functools.partial for this.

for i in range(3):
    self.button.append(Button(self, text='Game '+str(i+1), command=partial(self.open_this, i)))
lukad
  • 17,287
  • 3
  • 36
  • 53
  • seemed easy, if you are using virtual environments you might get an issue, refer to the first on: `self.button.append(Button(self, text='Game ' + str(i + 1), command=lambda x=i: self.open_this(x)))` – Lucem Mar 29 '20 at 09:15
  • 2
    @Lucem "if you are using virtual environments you might get an issue" There is no good reason why, as `functools.partial` is part of the standard library and has been for a long time. If you had difficulty with it then you should try to diagnose the problem and ask your own question. – Karl Knechtel May 11 '22 at 11:37
3

Simply attach your buttons scope within a lambda function like this:

btn["command"] = lambda btn=btn: click(btn) where click(btn) is the function that passes in the button itself. This will create a binding scope from the button to the function itself.

Features:

  • Customize gridsize
  • Responsive resizing
  • Toggle active state

#Python2
#from Tkinter import *
#import Tkinter as tkinter
#Python3
from tkinter import *
import tkinter

root = Tk()
frame=Frame(root)
Grid.rowconfigure(root, 0, weight=1)
Grid.columnconfigure(root, 0, weight=1)
frame.grid(row=0, column=0, sticky=N+S+E+W)
grid=Frame(frame)
grid.grid(sticky=N+S+E+W, column=0, row=7, columnspan=2)
Grid.rowconfigure(frame, 7, weight=1)
Grid.columnconfigure(frame, 0, weight=1)

active="red"
default_color="white"

def main(height=5,width=5):
  for x in range(width):
    for y in range(height):
      btn = tkinter.Button(frame, bg=default_color)
      btn.grid(column=x, row=y, sticky=N+S+E+W)
      btn["command"] = lambda btn=btn: click(btn)

  for x in range(width):
    Grid.columnconfigure(frame, x, weight=1)

  for y in range(height):
    Grid.rowconfigure(frame, y, weight=1)

  return frame

def click(button):
  if(button["bg"] == active):
    button["bg"] = default_color
  else:
    button["bg"] = active

w= main(10,10)
tkinter.mainloop()

enter image description here enter image description here

enter image description here

Joel
  • 5,732
  • 4
  • 37
  • 65
1

It's because the value for the name i changes and isn't captured by lambda:. (You can try that theory out by adding i = 1234 after the loop and seeing what happens.)

You'll need to write a function to wrap that i as a local name, then return a lambda in that function that captures i .

def make_button_click_command(i):
    return lambda: button_click(i)

# ...

btn = Button(..., command=make_button_click_command(i))

Another option is functools.partial, which does effectively the same thing:

command=functools.partial(button_click, i)

All in all, you can also simplify things a bit by using just range to get numbers from 0 to 10 and divmod to get the row and column in one function call:

from tkinter import Tk, Button


def button_click(i):
    print(i)


def make_button_click_command(i):
    return lambda: button_click(i)


root = Tk()

for i in range(10):
    value = (i + 1) % 10
    row, col = divmod(i, 3)
    btn = Button(root, text=value, padx=40, pady=20, command=make_button_click_command(value))
    btn.grid(row=row + 1, column=col)

root.mainloop()
AKX
  • 152,115
  • 15
  • 115
  • 172