Textbox (Entry) Validation in Tk (tkinter)

We have already seen in a previous post how to work with textboxes using the ttk.Entry class in a Tcl/Tk desktop application. Now let's see how to validate the text that the user writes within a certain textbox, for example, in order to allow only numbers, dates, or other formats, or to apply other types of restrictions.

Let's start with the following code, which creates a window with a textbox (entry).

from tkinter import ttk
import tkinter as tk
root = tk.Tk()
root.config(width=300, height=200)
root.title("My App")
entry = ttk.Entry()
entry.place(x=50, y=50, width=150)
root.mainloop()

Suppose we want the user to only be able to enter numbers. To do this, we first create a function that will be called every time the user types, pastes, or deletes text in the textbox:

def validate_entry(text):
    return text.isdecimal()

The text parameter is the string that the user is trying to type or paste into the textbox. If the user is typing, text will probably be a single character, while if he is pasting something from the clipboard, it can be an entire text. The isdecimal() string method returns True if the string is numeric-only, False otherwise. We are using the return value of isdecimal() as the result of our own function. This is very important for the validation process. When the result of validate_entry() is False, Tk will not allow the user to enter the character or text passed as argument.

Now let's tell our textbox the function that should be called to verify that the content is valid. To do so, change line 7 of the first code into:

entry = ttk.Entry(
    validate="key",
    validatecommand=(root.register(validate_entry), "%S")
)

Let's clarify this that looks a bit strange, but it's very simple. The first validate argument tells the textbox what type of validation we're looking for: "key" means text input. The second is a bit more complex: we pass a tuple that contains the function that is going to take care of validating the data (validate_entry(), which we must be previously registered via root.register()) and then a string denoting what information we want to receive as an argument in that function ("%S" means the text entered by the user).

Complete code:

from tkinter import ttk
import tkinter as tk
def validate_entry(text):
    return text.isdecimal()
root = tk.Tk()
root.config(width=300, height=200)
root.title("My App")
entry = ttk.Entry(
    validate="key",
    validatecommand=(root.register(validate_entry), "%S")
)
entry.place(x=50, y=50, width=150)
root.mainloop()

Run the program and voila! You will see that the user is no longer allowed to enter anything other than numbers.

Let's move on a bit. What happens if, in addition, we want the maximum number of entered characters to be ten? We would need to have in our validate_entry() function a string with the previous text of the textbox plus the text that is about to be entered, in order to calculate its length and return False in case that it has more than ten characters. So let's tell Tk we want that data in our function, by adding the string "%P" to the validatecommand argument:

entry = ttk.Entry(
    validate="key",
    validatecommand=(root.register(validate_entry), "%S", "%P")
)

And let's change the definition of our function, which will now take two arguments:

def validate_entry(text, new_text):
    return text.isdecimal()

text is the text that the user wants to enter (before it is actually inserted!) and new_text is the text already contained in the textbox (if any) plus text. Now let's apply the new restriction:

def validate_entry(text, new_text):
    # First check that the entire content has no more than ten characters.
    if len(new_text) > 10:
        return False
    # Then make sure the text is numeric-only.
    return text.isdecimal()

Great! Let's look at one last example.

Let's say our text box only accepts dates in dd/mm/yyyy format (for example, 14/08/2022). What restrictions can we apply? First of all we must make sure that the text has no more than ten characters, just like before. The next thing would be to check that dd, mm and yyyy are decimal numbers. Finally, that between the day and the month and between the month and the year there is a / (slash) that works as a separator.

def validate_entry(new_text):
    """
    Make sure `new_text` has the dd/mm/yyyy format.
    """
    # No more than ten characters.
    if len(new_text) > 10:
        return False
    checks = []
    for i, char in enumerate(new_text):
        # Indexes 2 and 5 must have the "/" character.
        if i in (2, 5):
            checks.append(char == "/")
        else:
            # The remaining characters must be numbers between 0 and 9.
            checks.append(char.isdecimal())
    # `all()` returns True if all checks are True.
    return all(checks)

Since we're only using the new_text argument, we're going to modify validatecommand on the widget creation:

entry = ttk.Entry(
    validate="key",
    # We just need "%P".
    validatecommand=(root.register(validate_entry), "%P")
)

Perfect! Now our textbox only accepts dates in the specified format.

Other possible options for validatecommand are "%d", "%i" and "%s" (note the lowercase). "%d" provides an argument that determines the action being validated ("1" when adding text, "0" on deletion), which is particularly useful if you want to apply validation when text is added but not when it is deleted, or vice-versa. "%i" indicates the position where the text is added. "%s" (lowercase) is the content of the textbox before the modification. We can observe all these values with the following code:

from tkinter import ttk
import tkinter as tk
def validate_entry(action, index, new_text, previous_text, text):
    print("Action:", action)
    print("Index (position) where the text is to be inserted:", index)
    print("Text if validation is True:", new_text)
    print("Previous content of the textbox:", previous_text)
    print("Text to be inserted:", text)
    return True
root = tk.Tk()
root.config(width=300, height=200)
root.title("My App")
entry = ttk.Entry(
    validate="key",
    validatecommand=(
        root.register(validate_entry),
        "%d", "%i", "%P", "%s", "%S"
    )
)
entry.place(x=50, y=50, width=150)
root.mainloop()

In summary, to validate the insertion or removal of characters entered by the user in a textbox, we must:

  1. Create a function with the validation logic that returns True (allow) or False (don't allow).

  2. Put the name of the function and the arguments we want it to receive ("%d", "%i", etc.) in validatecommand when creating the textbox (as we've shown) by using root.register().

  3. Add the validate="key" argument when creating the textbox widget.

Comments