2

I have this code:

from tkinter import *
import tkinter as tk

class App(tk.Frame):
    def __init__(self, master):
        def print_test(self):
            print('test')

        def button_click():
            print_test()

        super().__init__(master)
        master.geometry("250x100")
        entry = Entry()

        test = DoubleVar()
        entry["textvariable"] = test
        entry.bind('<Key-Return>', print_test)
        entry.pack()
        button = Button(root, text="Click here", command=button_click)
        button.pack()

root = tk.Tk()
myapp = App(root)
myapp.mainloop()

A click on the button throws:

Exception in Tkinter callback
Traceback (most recent call last):
  File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\lib\tkinter\__init__.py", line 1921, in __call__
    return self.func(*args)
  File "[somefilepath]", line 10, in button_click
    print_test()
TypeError: App.__init__.<locals>.print_test() missing 1 required positional argument: 'self'

While pressing Enter while in the Entry widget works, it prints: test

See:

enter image description here

Now if I drop the (self) from def print_test(self):, as TypeError: button_click() missing 1 required positional argument: 'self' shows, the button works, but pressing Enter in the Entry widget does not trigger the command but throws another exception:

enter image description here

Exception in Tkinter callback
Traceback (most recent call last):
  File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\lib\tkinter\__init__.py", line 1921, in __call__
    return self.func(*args)
TypeError: App.__init__.<locals>.print_test() takes 0 positional arguments but 1 was given

How to write the code so that both the button click event and pressing Enter will trigger the print command?

questionto42
  • 7,175
  • 4
  • 57
  • 90
  • 1
    Just to make sure you understand: the argument that gets passed to `print_test` from the keybind has **nothing to do with** your class; it's an instance of a Tkinter event - so naming it `self` does not make any sense. – Karl Knechtel May 18 '23 at 00:54
  • 1
    Please look into the Q&A for the [event parameter](https://stackoverflow.com/q/68356340/13629335). You basically misunderstand what is parsed to the "nested function". There is also a great answer to "[what self is](https://stackoverflow.com/a/2709832)" on [so] and when to use this "name". – Thingamabobs May 18 '23 at 09:20

2 Answers2

3

Commands callbacks for button clicks are called without arguments, because there is no more information that is relevant: the point of a button is that there's only one "way" to click it.

However, key presses are events, and as such, callbacks for key-binds are passed an argument that represents the event (not anything to do with the context in which the callback was written).

For key-press handlers, it's usually not necessary to consider any information from the event. As such, the callback can simply default this parameter to None, and then ignore it:

def print_test(event=None):
    print('test')

Now this can be used directly as a handler for both the key-bind and the button press. Note that this works perfectly well as a top-level function, even outside of the App class, because the code uses no functionality from App.

Another way is to reverse the delegation logic. In the original code, the button handler tries to delegate to the key-press handler, but cannot because it does not have an event object to pass. While it would work to pass None or some other useless object (since the key-press handler does not actually care about the event), this is a bit ugly. A better way is to delegate the other way around: have the key-press handler discard the event that was passed to it, as it delegates to the button handler (which performs a hard-coded action).

Thus:

from tkinter import *
import tkinter as tk

def print_test():
    print('test')

def enter_pressed(event):
    print_test()

class App(tk.Frame):
    def __init__(self, master):
        super().__init__(master)
        master.geometry("250x100")

        entry = Entry()
        test = DoubleVar()
        entry["textvariable"] = test
        entry.bind('<Key-Return>', enter_pressed)
        entry.pack()

        button = Button(root, text="Click here", command=print_test)
        button.pack()


root = tk.Tk()
myapp = App(root)
myapp.mainloop()
Karl Knechtel
  • 62,466
  • 11
  • 102
  • 153
1

Mind, this answer is wrong, it is kept only to show the mistake

The name "self" does not make any sense here. The answer works as such but is not the right way to name the event parameter since the passed "tk.Event" argument has nothing to do with the class from where the print_test function is called. Check the other answer and comments for the needed parameter event instead of self (or you could name the event whatever, for example tkevent to show that it is a tkinter event, but not confusingly "self". Naming it "event" or "tkevent" or "tk_event" or the like makes clear rightaway that this is about the tk.Event and nothing else). I leave this self=None/self answer just for a better understanding of this mistake, do not name the tkinter event "self".


Answer (writing self works but is wrong here)

Naming the event argument self mixes up the naming of the event of the button with self of the class. The class and the button event have nothing to do with each other, therefore the parameter names should not show any link between the two.

Make the argument optional with self=None:

from tkinter import *
import tkinter as tk

class App(tk.Frame):
    def __init__(self, master):
        def print_test(self=None):
            print('test')

        def button_click():
            print_test()

        super().__init__(master)
        master.geometry("200x50")
        entry = Entry()

        test = DoubleVar()
        entry["textvariable"] = test
        entry.bind('<Key-Return>', print_test)
        entry.pack()
        button = Button(root, text="Click here", command=button_click)
        button.pack()

root = tk.Tk()
myapp = App(root)
myapp.mainloop()

enter image description here

Taking up the comment by Bryan Oakley, here is why this is needed:

a bound function (entry.bind('<Key-Return>', print_test)) is passed an argument while a function called via the command attribute (command=button_click) is not.

Another way to make both triggers work without an error is to call print_test(self) instead of print_test() from the button_click function. Easy as it seems, I did not in a more complex code.

from tkinter import *
import tkinter as tk

class App(tk.Frame):
    def __init__(self, master):
        def print_test(self):
            print('test')

        def button_click():
            print_test(self)

        super().__init__(master)
        master.geometry("200x50")
        entry = Entry()

        test = DoubleVar()
        entry["textvariable"] = test
        entry.bind('<Key-Return>', print_test)
        entry.pack()
        button = Button(root, text="Click here", command=button_click)
        button.pack()

root = tk.Tk()
myapp = App(root)
myapp.mainloop()

Out again:

enter image description here

questionto42
  • 7,175
  • 4
  • 57
  • 90
  • 1
    This answer would be better if you explained why this was necessary: that a bound function is passed an argument while a function called via the `command` attribute is not. – Bryan Oakley May 17 '23 at 22:43
  • @BryanOakley I frankly did not know why. I guessed that the problem is that the one function is passed without argument, but I somehow did not make the step to think the other way round for the other function. Thanks for making this clear. – questionto42 May 17 '23 at 23:14