Tkinter Basics: Windows, Buttons, and Inputs

The same loop, wearing different clothes

Advanced 13 min

In this lesson

Tkinter makes desktop applications (windows with buttons, text boxes and labels) and it ships with Python, so there is nothing to install.

It looks like a completely different world from Pygame. It is not. There is still an event loop; Tkinter just runs it for you and calls your functions when something happens.

Explain it like I’m 5

Tkinter is the same waiting-and-responding loop as a game, except you never write the loop. You describe the buttons and say what each one should do.

Widgets, layout, and the loop you do not write

Three ideas cover most of Tkinter.

Widgets are the pieces: Label for text, Button for clicking, Entry for typing.

Layout decides where they go. .grid(row=, column=) arranges them in a table and is the one to learn. .pack() is quicker for simple stacks but harder to control.

The loop is root.mainloop(). It does exactly what your Pygame while loop did, waiting for events forever, but instead of you checking for them, it calls the function you attached to each widget. That inversion is the whole difference.

Example · shopping_list.py
import tkinter as tk

def add_item():
    """Called by Tkinter whenever the button is clicked."""
    item = entry.get().strip()
    if not item:
        status.config(text="type something first")
        return
    items.append(item)
    entry.delete(0, tk.END)
    status.config(text=f"{len(items)} item(s): {', '.join(items)}")

items = []

root = tk.Tk()
root.title("Shopping list")

tk.Label(root, text="Item:").grid(row=0, column=0, padx=8, pady=8)

entry = tk.Entry(root, width=24)
entry.grid(row=0, column=1, padx=8, pady=8)

tk.Button(root, text="Add", command=add_item).grid(row=0, column=2, padx=8)

status = tk.Label(root, text="nothing yet")
status.grid(row=1, column=0, columnspan=3, pady=8)

root.mainloop()
Output
# A small window with a text box, an Add button and a status line.
# Typing "milk" and clicking Add shows: 1 item(s): milk
# Clicking Add with an empty box shows: type something first
A working desktop app. Run locally with: python shopping_list.py

Callbacks are just functions with state

A callback is a function you write and someone else calls. Every button gets one, and its job is always the same: change the state, then update what is displayed.

Which means — as with the game loop — the interesting part has nothing to do with the toolkit. Strip the widgets away and a Tkinter app is a dictionary of state plus a set of functions that modify it.

Example
# The callback half of a GUI, with no Tkinter involved.
state = {"count": 0}

def on_click():
    state["count"] += 1
    return f"clicked {state['count']} times"

def on_reset():
    state["count"] = 0
    return "clicked 0 times"

print(on_click())
print(on_click())
print(on_click())
print(on_reset())
print("state:", state)
Output
clicked 1 times
clicked 2 times
clicked 3 times
clicked 0 times
state: {'count': 0}
Two buttons' worth of behavior, testable without a window.

Write the callbacks for a running total with an undo button. add(amount) adds to the total and remembers the amount; undo() removes the most recent amount and subtracts it. Both return the new total as a message, and undo() on an empty history must say so rather than crashing.

state = {"total": 0, "history": []}

def add(amount):
    # TODO: add to the total, remember the amount, return "total: N"
    return ""

def undo():
    # TODO: if there is nothing to undo, return "nothing to undo"
    # TODO: otherwise remove the last amount, subtract it, return "total: N"
    return ""

print(add(5))
print(add(3))
print(undo())
print(undo())
print(undo())

Pygame or Tkinter?

They solve different problems and the choice is usually obvious once stated.

Tkinter is for applications: forms, buttons, lists, settings. It waits patiently and does nothing until the user acts. Reach for it when your program is a tool.

Pygame is for anything that moves on its own. It redraws continuously whether or not anyone touches the keyboard. Reach for it when time passes inside your program.

Underneath they are the same loop, which is why the last two lessons kept producing code you could test without either library. Learn that separation and the toolkit becomes a detail you can swap.

Common mistake: Writing command=my_function() with brackets

Why it happens:

Brackets are how functions are normally called.

How to fix it:

Pass the function itself: command=my_function. With brackets it runs once at startup and the button does nothing.

Common mistake: Putting code after root.mainloop()

Why it happens:

It looks like a setup call that returns.

How to fix it:

mainloop() blocks until the window closes. Anything after it waits until the app has already finished.

Common mistake: Mixing grid() and pack() in the same container

Why it happens:

Both position widgets, so they look interchangeable.

How to fix it:

They are different layout managers and mixing them in one parent freezes the window. Pick one per container.

Common mistake: Forgetting to keep a reference to a widget you want to change

Why it happens:

Chaining .grid() onto the creation line is tidy, and returns None.

How to fix it:

If you need to update it later, assign first and call .grid() on the next line.

What does root.mainloop() do?

Why is command=add_item written without brackets?

When is Pygame the better choice than Tkinter?

Mini exercise (medium)

Write the callback logic for a simple login form, the part that would sit behind a Tkinter button. submit(username, password) should reject an empty username, reject a password under 8 characters, and otherwise accept. It returns a (ok, message) pair and records failed attempts in the state.

Have a go. Finish the code and press Run to see the result immediately, right on this page.

state = {"failures": 0}

def submit(username, password):
    """Return (ok, message). Count failed attempts in state."""
    # TODO: reject an empty username
    # TODO: reject a password shorter than 8 characters
    # TODO: otherwise accept, welcoming the user by name
    return False, ""

print(submit("", "whatever"))
print(submit("ada", "short"))
print(submit("ada", "correcthorse"))
print("failures:", state["failures"])

What to learn next

You built a desktop app: widgets, .grid() for layout, command=my_function with no brackets, reading an Entry and updating a Label, and root.mainloop() running the same loop you wrote by hand in Pygame. You also saw that the callbacks are just functions over state, testable with no window at all.

Now prove it. The Unit 14 Project builds one quiz engine and puts two completely different faces on it without changing a rule.