109

What is the recommended technique for interactively validating content in a tkinter Entry widget?

I've read the posts about using validate=True and validatecommand=command, and it appears that these features are limited by the fact that they get cleared if the validatecommand command updates the Entry widget's value.

Given this behavior, should we bind on the KeyPress, Cut, and Paste events and monitor/update our Entry widget's value through these events? (And other related events that I might have missed?)

Or should we forget interactive validation altogether and only validate on FocusOut events?

Paolo Forgia
  • 6,572
  • 8
  • 46
  • 58
Malcolm
  • 5,125
  • 10
  • 52
  • 75

10 Answers10

282

The correct answer is, use the validatecommand attribute of the widget. Unfortunately this feature is severely under-documented in the Tkinter world, though it is quite sufficiently documented in the Tk world. Even though it's not documented well, it has everything you need to do validation without resorting to bindings or tracing variables, or modifying the widget from within the validation procedure.

The trick is to know that you can have Tkinter pass in special values to your validate command. These values give you all the information you need to know to decide on whether the data is valid or not: the value prior to the edit, the value after the edit if the edit is valid, and several other bits of information. To use these, though, you need to do a little voodoo to get this information passed to your validate command.

Note: it's important that the validation command returns either True or False. Anything else will cause the validation to be turned off for the widget.

Here's an example that only allows lowercase. It also prints the values of all of the special values for illustrative purposes. They aren't all necessary; you rarely need more than one or two.

import tkinter as tk  # python 3.x
# import Tkinter as tk # python 2.x

class Example(tk.Frame):

    def __init__(self, parent):
        tk.Frame.__init__(self, parent)

        # valid percent substitutions (from the Tk entry man page)
        # note: you only have to register the ones you need; this
        # example registers them all for illustrative purposes
        #
        # %d = Type of action (1=insert, 0=delete, -1 for others)
        # %i = index of char string to be inserted/deleted, or -1
        # %P = value of the entry if the edit is allowed
        # %s = value of entry prior to editing
        # %S = the text string being inserted or deleted, if any
        # %v = the type of validation that is currently set
        # %V = the type of validation that triggered the callback
        #      (key, focusin, focusout, forced)
        # %W = the tk name of the widget

        vcmd = (self.register(self.onValidate),
                '%d', '%i', '%P', '%s', '%S', '%v', '%V', '%W')
        self.entry = tk.Entry(self, validate="key", validatecommand=vcmd)
        self.text = tk.Text(self, height=10, width=40)
        self.entry.pack(side="top", fill="x")
        self.text.pack(side="bottom", fill="both", expand=True)

    def onValidate(self, d, i, P, s, S, v, V, W):
        self.text.delete("1.0", "end")
        self.text.insert("end","OnValidate:\n")
        self.text.insert("end","d='%s'\n" % d)
        self.text.insert("end","i='%s'\n" % i)
        self.text.insert("end","P='%s'\n" % P)
        self.text.insert("end","s='%s'\n" % s)
        self.text.insert("end","S='%s'\n" % S)
        self.text.insert("end","v='%s'\n" % v)
        self.text.insert("end","V='%s'\n" % V)
        self.text.insert("end","W='%s'\n" % W)

        # Disallow anything but lowercase letters
        if S == S.lower():
            return True
        else:
            self.bell()
            return False

if __name__ == "__main__":
    root = tk.Tk()
    Example(root).pack(fill="both", expand=True)
    root.mainloop()

For more information about what happens under the hood when you call the register method, see Why is calling register() required for tkinter input validation?

For the canonical documentation see the Validation section of the Tcl/Tk Entry man page

Bryan Oakley
  • 370,779
  • 53
  • 539
  • 685
  • 20
    This is the right way to do it. It addresses the problems I found when I tried to get jmeyer10's answer working. This one example provides superior documentation to validate compared to what I can find elsewhere. I wish I could give this 5 votes. – Steven Rumbalski Nov 10 '10 at 03:34
  • 9
    WOW! I agree with Steven - this is the type of reply that deserves more than one vote. You should write a book on Tkinter (and you've already posted enough solutions to make that a multi-volume series). Thank you!!! – Malcolm Nov 10 '10 at 13:31
  • 1
    As a side note: I'm blown away by the power of Tkinter. Its gotten a bad wrap for many years, but with the new support for native themes (ttk) and explanations of its hidden power powers by experts like Bryan Oakley, this GUI framework can hold its own against the likes of wxPython, pyQT, and others. – Malcolm Nov 10 '10 at 13:35
  • 2
    Thanks for the example. It's worth noting that the validatecommand MUST return a boolean (only True and False). If not, the validation will be removed. – Dave Bacher Jun 19 '12 at 05:50
  • `self.text = tk.Text(self, height=10, width=40)` I have a question about this code 7 years later~ why the component of `self` can be constructed with the parent MASTER as `self` itself???? Shouldn't it like this `self.text = tk.Text(parent, height=10, width=40)` – MMMMMCCLXXVII Mar 05 '17 at 14:58
  • @MMMMMCCLXXVII: because `self` is a frame and the text widget is a child of that frame. `parent` is the parent of `self`. – Bryan Oakley Mar 05 '17 at 15:24
  • @BryanOakley I see!! Another question. If I re-write your code `tk.Frame.__init__(self, parent)` to `super(Example,self).__init__(parent)`, it will throw an error `TypeError: super() argument 1 must be type, not classobj`. Why is that? How to use `super` in your case? – MMMMMCCLXXVII Mar 06 '17 at 06:12
  • @MMMMMCCLXXVII: stackoverflow isn't a discussion site. If you have questions, please click the "Ask Question" button. – Bryan Oakley Mar 06 '17 at 12:55
  • The example works flawless. However I fail to integrate this into my code. I get AttributeError: 'MyClass' object has no attribute 'register'. Well the error is for sure linked to that 'self'. When I dump it in the example it says "... instance at 0x..." while in mycode it is just ",.. object at 0x...". So seem that voodoo-part of the code only work for code in instances and not for 'static' code. Gosh I guess need do an other round in learning about OOP in Python. – Nadu Jul 12 '17 at 17:59
  • @Nadu: no, there's no special voodoo. `register` is a common method on all widgets. In this example `self` is a `Frame` widget. You can use the `register` method of any widget (eg: `root.register(...)`) – Bryan Oakley Jul 12 '17 at 18:13
  • 5
    I think [this page](http://infohost.nmt.edu/tcc/help/pubs/tkinter/web/entry-validation.html) should be brought to the fore. – Right leg Sep 08 '17 at 13:57
  • 8
    "severely under-documented in the Tkinter world". LOL — like almost all of the rest of the Tkiinter world. – martineau Aug 16 '18 at 15:52
  • 5
    @Rightleg That page doesn't exist anymore. Archived version: http://web.archive.org/web/20190423043443/http://infohost.nmt.edu/tcc/help/pubs/tkinter/web/entry-validation.html – Maximouse Feb 17 '20 at 15:48
  • Will we have to add validation to each widget if we wanted validation for multiple entry widgets? – Delrius Euphoria Oct 13 '20 at 04:19
  • @CoolCloud: I'm not sure what you mean. You have to configure the validation options for each widget, though you could probably use the option database to specify defaults. You can also create your own custom Entry class that does it automatically. – Bryan Oakley Oct 13 '20 at 13:43
  • @BryanOakley Oh rather than applying validation to all class, i think making a class with validation is better, Thanks ! – Delrius Euphoria Oct 13 '20 at 13:53
  • How can the `'%W'` option be used? Anyway to map the name to a `tkinter` widget variable (which could be useful)? – martineau Feb 07 '21 at 14:41
  • 2
    @martineau: you can use the common `nametowidget` method to convert the name to the actual widget object. – Bryan Oakley Feb 07 '21 at 23:25
  • 1
    Bryan: Thank you. Eventually on my own I found [some info on that method](https://stackoverflow.com/a/8895847/355230) — knew something like `nametowidget()` existed but couldn't remember its name nor exactly how to use it. – martineau Feb 08 '21 at 01:35
25

After studying and experimenting with Bryan's code, I produced a minimal version of input validation. The following code will put up an Entry box and only accept numeric digits.

from tkinter import *

root = Tk()

def testVal(inStr,acttyp):
    if acttyp == '1': #insert
        if not inStr.isdigit():
            return False
    return True

entry = Entry(root, validate="key")
entry['validatecommand'] = (entry.register(testVal),'%P','%d')
entry.pack()

root.mainloop()

Perhaps I should add that I am still learning Python and I will gladly accept any and all comments/suggestions.

JCKE
  • 386
  • 5
  • 15
user1683793
  • 1,213
  • 13
  • 18
  • 5
    Generally people use `entry.configure(validatecommand=...)` and write `test_val` instead of `testVal`, but this is a good, simple example. – wizzwizz4 Aug 12 '18 at 14:58
  • Wouldn't you need to also allow a decimal point? `".".isdigit()` → `False` – martineau Jun 26 '22 at 15:06
  • @martineau The answerer intended this code to only accept "numeric digits". This intentionally doesn't include the ".". "." is not a numeric digit. The code wasn't meant to accept all numbers, it was meant to accept "numeric digits". – Xbox One Apr 23 '23 at 01:06
14

Use a Tkinter.StringVar to track the value of the Entry widget. You can validate the value of the StringVar by setting a trace on it.

Here's a short working program that accepts only valid floats in the Entry widget.

try:
    from tkinter import *
except ImportError:
    from Tkinter import *  # Python 2


root = Tk()
sv = StringVar()

def validate_float(var):
    new_value = var.get()
    try:
        new_value == '' or float(new_value)
        validate_float.old_value = new_value
    except:
        var.set(validate_float.old_value)

validate_float.old_value = ''  # Define function attribute.

# trace wants a callback with nearly useless parameters, fixing with lambda.
sv.trace('w', lambda nm, idx, mode, var=sv: validate_float(var))
ent = Entry(root, textvariable=sv)
ent.pack()
ent.focus_set()

root.mainloop()

martineau
  • 119,623
  • 25
  • 170
  • 301
Steven Rumbalski
  • 44,786
  • 9
  • 89
  • 119
  • 4
    Thanks for your post. I enjoyed seeing the Tkinter StringVar .trace() method in use. – Malcolm Nov 10 '10 at 13:38
  • any idea why I could possibly get this error? "NameError: name 'validate' is not defined" – Armen Sanoyan Dec 15 '20 at 16:22
  • 2
    @ArmenSanoyan: It's because `validate` is not defined in this snippet (and that should be corrected). – martineau Feb 07 '21 at 14:34
  • That's a really great idea in some cases but what about fixing the indicated issue around the `validate` variable? – Wolf Apr 15 '21 at 15:22
  • 2
    @Wolf: See update I made since answer's author does not appear interested in fixing it themselves… – martineau Aug 13 '21 at 18:49
  • Thanks, @martineau, for taking up the cause. – Wolf Aug 13 '21 at 21:59
  • 2
    @Wolf: While fixing things here I noticed some other deficiencies and decided to post an [answer](https://stackoverflow.com/a/68779709/355230) of my own to address them. – martineau Aug 14 '21 at 01:40
  • @martineau Thanks for letting me know. Posting this implementation of the core idea makes sense. – Wolf Aug 14 '21 at 09:39
8

Bryan's answer is correct, however no one mentioned the 'invalidcommand' attribute of the tkinter widget.

A good explanation is here: http://infohost.nmt.edu/tcc/help/pubs/tkinter/web/entry-validation.html

Text copy/pasted in case of broken link

The Entry widget also supports an invalidcommand option that specifies a callback function that is called whenever the validatecommand returns False. This command may modify the text in the widget by using the .set() method on the widget's associated textvariable. Setting up this option works the same as setting up the validatecommand. You must use the .register() method to wrap your Python function; this method returns the name of the wrapped function as a string. Then you will pass as the value of the invalidcommand option either that string, or as the first element of a tuple containing substitution codes.

Note: There is only one thing that I cannot figure out how to do: If you add validation to an entry, and the user selects a portion of the text and types a new value, there is no way to capture the original value and reset the entry. Here's an example

  1. Entry is designed to only accept integers by implementing 'validatecommand'
  2. User enters 1234567
  3. User selects '345' and presses 'j'. This is registered as two actions: deletion of '345', and insertion of 'j'. Tkinter ignores the deletion and acts only on the insertion of 'j'. 'validatecommand' returns False, and the values passed to the 'invalidcommand' function are as follows: %d=1, %i=2, %P=12j67, %s=1267, %S=j
  4. If the code does not implement an 'invalidcommand' function, the 'validatecommand' function will reject the 'j' and the result will be 1267. If the code does implement an 'invalidcommand' function, there is no way to recover the original 1234567.
K.Dᴀᴠɪs
  • 9,945
  • 11
  • 33
  • 43
orionrobert
  • 101
  • 1
  • 5
6

Define a function returning a boolean that indicates whether the input is valid.
Register it as a Tcl callback, and pass the callback name to the widget as a validatecommand.

For example:

import tkinter as tk


def validator(P):
    """Validates the input.

    Args:
        P (int): the value the text would have after the change.

    Returns:
        bool: True if the input is digit-only or empty, and False otherwise.
    """

    return P.isdigit() or P == ""


root = tk.Tk()

entry = tk.Entry(root)
entry.configure(
    validate="key",
    validatecommand=(
        root.register(validator),
        "%P",
    ),
)
entry.grid()

root.mainloop()

Reference.

Demian Wolf
  • 1,698
  • 2
  • 14
  • 34
5

While studying Bryan Oakley's answer, something told me that a far more general solution could be developed. The following example introduces a mode enumeration, a type dictionary, and a setup function for validation purposes. See line 48 for example usage and a demonstration of its simplicity.

#! /usr/bin/env python3
# https://stackoverflow.com/questions/4140437
import enum
import inspect
import tkinter
from tkinter.constants import *


Mode = enum.Enum('Mode', 'none key focus focusin focusout all')
CAST = dict(d=int, i=int, P=str, s=str, S=str,
            v=Mode.__getitem__, V=Mode.__getitem__, W=str)


def on_validate(widget, mode, validator):
    # http://www.tcl.tk/man/tcl/TkCmd/ttk_entry.htm#M39
    if mode not in Mode:
        raise ValueError('mode not recognized')
    parameters = inspect.signature(validator).parameters
    if not set(parameters).issubset(CAST):
        raise ValueError('validator arguments not recognized')
    casts = tuple(map(CAST.__getitem__, parameters))
    widget.configure(validate=mode.name, validatecommand=[widget.register(
        lambda *args: bool(validator(*(cast(arg) for cast, arg in zip(
            casts, args)))))]+['%' + parameter for parameter in parameters])


class Example(tkinter.Frame):

    @classmethod
    def main(cls):
        tkinter.NoDefaultRoot()
        root = tkinter.Tk()
        root.title('Validation Example')
        cls(root).grid(sticky=NSEW)
        root.grid_rowconfigure(0, weight=1)
        root.grid_columnconfigure(0, weight=1)
        root.mainloop()

    def __init__(self, master, **kw):
        super().__init__(master, **kw)
        self.entry = tkinter.Entry(self)
        self.text = tkinter.Text(self, height=15, width=50,
                                 wrap=WORD, state=DISABLED)
        self.entry.grid(row=0, column=0, sticky=NSEW)
        self.text.grid(row=1, column=0, sticky=NSEW)
        self.grid_rowconfigure(1, weight=1)
        self.grid_columnconfigure(0, weight=1)
        on_validate(self.entry, Mode.key, self.validator)

    def validator(self, d, i, P, s, S, v, V, W):
        self.text['state'] = NORMAL
        self.text.delete(1.0, END)
        self.text.insert(END, 'd = {!r}\ni = {!r}\nP = {!r}\ns = {!r}\n'
                              'S = {!r}\nv = {!r}\nV = {!r}\nW = {!r}'
                         .format(d, i, P, s, S, v, V, W))
        self.text['state'] = DISABLED
        return not S.isupper()


if __name__ == '__main__':
    Example.main()
Community
  • 1
  • 1
Noctis Skytower
  • 21,433
  • 16
  • 79
  • 117
4
import tkinter
tk=tkinter.Tk()
def only_numeric_input(e):
    #this is allowing all numeric input
    if e.isdigit():
        return True
    #this will allow backspace to work
    elif e=="":
        return True
    else:
        return False
#this will make the entry widget on root window
e1=tkinter.Entry(tk)
#arranging entry widget on screen
e1.grid(row=0,column=0)
c=tk.register(only_numeric_input)
e1.configure(validate="key",validatecommand=(c,'%P'))
tk.mainloop()
#very usefull for making app like calci
Marc.2377
  • 7,807
  • 7
  • 51
  • 95
  • 3
    Hi, welcome to Stack Overflow. "Code-only" answers are frowned upon, especially when answering a question that already has many answers. Please be sure to add some additional insight into why the response you're providing is somehow substantive and not simply echoing what's already been vetted by the original poster. – chb Jan 31 '19 at 20:59
  • 1
    @Demian Wolf I liked your improved version of the original answer, but I had to roll it back. Please, consider posting it as an answer of your own (you can find it in the [revision history](https://stackoverflow.com/posts/54468996/revisions)). – Marc.2377 Jun 16 '19 at 03:34
4

This code can help if you want to set both just digits and max characters.

from tkinter import *

root = Tk()

def validate(P):
    if len(P) == 0 or len(P) <= 10 and P.isdigit():  # 10 characters
        return True
    else:
        return False

ent = Entry(root, validate="key", validatecommand=(root.register(validate), '%P'))
ent.pack()

root.mainloop()
4

Here's an improved version of @Steven Rumbalski's answer of validating the Entry widgets value by tracing changes to a StringVar — which I have already debugged and improved to some degree by editing it in place.

The version below puts everything into a StringVar subclass to encapsulates what's going on better and, more importantly allow multiple independent instances of it to exist at the same time without interfering with each other — a potential problem with his implementation because it utilizes function attributes instead of instance attributes, which are essentially the same thing as global variables and can lead to problems in such a scenario.

try:
    from tkinter import *
except ImportError:
    from Tkinter import *  # Python 2


class ValidateFloatVar(StringVar):
    """StringVar subclass that only allows valid float values to be put in it."""

    def __init__(self, master=None, value=None, name=None):
        StringVar.__init__(self, master, value, name)
        self._old_value = self.get()
        self.trace('w', self._validate)

    def _validate(self, *_):
        new_value = self.get()
        try:
            new_value == '' or float(new_value)
            self._old_value = new_value
        except ValueError:
            StringVar.set(self, self._old_value)


root = Tk()
ent = Entry(root, textvariable=ValidateFloatVar(value=42.0))
ent.pack()
ent.focus_set()
ent.icursor(END)

root.mainloop()

martineau
  • 119,623
  • 25
  • 170
  • 301
  • Extending it to work with multiple instances is an important improvement, the original “solution” would introduce a massive problem (bad code structure) when fixing another one. – Wolf Aug 14 '21 at 09:37
  • @Wolf: Obviously I agree. I felt the original author's idea of using tkinter's `StringVar` tracing capabilities to do the validation has merit, but the code in the implementation that was posted needed some work to be really be viable. – martineau Aug 14 '21 at 09:56
1

Responding to orionrobert's problem of dealing with simple validation upon substitutions of text through selection, instead of separate deletions or insertions:

A substitution of selected text is processed as a deletion followed by an insertion. This may lead to problems, for example, when the deletion should move the cursor to the left, while a substitution should move the cursor to the right. Fortunately, these two processes are executed immediately after one another. Hence, we can differentiate between a deletion by itself and a deletion directly followed by an insertion due to a substitution because the latter has does not change the idle flag between deletion and insertion.

This is exploited using a substitutionFlag and a Widget.after_idle(). after_idle() executes the lambda-function at the end of the event queue:

class ValidatedEntry(Entry):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self.tclValidate = (self.register(self.validate), '%d', '%i', '%P', '%s', '%S', '%v', '%V', '%W')
        # attach the registered validation function to this spinbox
        self.config(validate = "all", validatecommand = self.tclValidate)

    def validate(self, type, index, result, prior, indelText, currentValidationMode, reason, widgetName):

        if typeOfAction == "0":
            # set a flag that can be checked by the insertion validation for being part of the substitution
            self.substitutionFlag = True
            # store desired data
            self.priorBeforeDeletion = prior
            self.indexBeforeDeletion = index
            # reset the flag after idle
            self.after_idle(lambda: setattr(self, "substitutionFlag", False))

            # normal deletion validation
            pass

        elif typeOfAction == "1":

            # if this is a substitution, everything is shifted left by a deletion, so undo this by using the previous prior
            if self.substitutionFlag:
                # restore desired data to what it was during validation of the deletion
                prior = self.priorBeforeDeletion
                index = self.indexBeforeDeletion

                # optional (often not required) additional behavior upon substitution
                pass

            else:
                # normal insertion validation
                pass

        return True

Of course, after a substitution, while validating the deletion part, one still won’t know whether an insert will follow. Luckily however, with: .set(), .icursor(), .index(SEL_FIRST), .index(SEL_LAST), .index(INSERT), we can achieve most desired behavior retrospectively (since the combination of our new substitutionFlag with an insertion is a new unique and final event.

Stendert
  • 97
  • 7