TkDocs / Overview
Python 3.12 · Tk 8.6

TkinterThe Complete Python GUI Reference

Everything you need to build desktop applications with Python's standard GUI toolkit — beautifully documented with real examples.

What is Tkinter?

Tkinter is Python's standard GUI library — a thin object-oriented layer on top of Tcl/Tk, a mature cross-platform windowing toolkit. It ships with every standard Python distribution. No extra installation required.

With Tkinter you can build windows, dialogs, forms, data-entry tools, visualizations, and full desktop applications that run on Windows, macOS, and Linux from a single codebase.

✦ Classic tk widgets
  • Built directly into Tk
  • Full option control
  • Consistent cross-platform look
  • Simpler mental model
✦ Themed ttk widgets
  • Native OS appearance
  • Style / theme engine
  • Extra widgets: Treeview, Notebook…
  • Preferred for modern apps

Browse the Documentation

A Minimal Example

hello.py
import tkinter as tk
from tkinter import ttk

def greet():
    label.config(text=f"Hello, {name.get()}!")

root = tk.Tk()
root.title("Hello Tkinter")
root.geometry("300x120")

name = tk.StringVar()
ttk.Entry(root, textvariable=name).pack(pady=12)
ttk.Button(root, text="Greet", command=greet).pack()
label = ttk.Label(root, text="")
label.pack(pady=8)

root.mainloop()
ℹ️ Python Version

This documentation targets Python 3.8+ with Tk 8.6. Most examples work on older versions, but some features (like ttk.Spinbox) require Tk 8.6.

Installation

Tkinter ships with Python. Verify it is available:

terminal
python -c "import tkinter; print(tkinter.TkVersion)"
# Should print: 8.6
⚠️ Linux Users

On Ubuntu/Debian, run: sudo apt-get install python3-tk

Your First Window

Every Tkinter app follows the same three-step pattern:

  1. Create the root windowtk.Tk()
  2. Add widgets inside it
  3. Start the event looproot.mainloop()
first_window.py
import tkinter as tk

root = tk.Tk()
root.title("My First App")
root.geometry("400x300")  # Width x Height
root.mainloop()

Complete App: Temperature Converter

temp_converter.py
import tkinter as tk
from tkinter import ttk

class TempConverter(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("Temperature Converter")
        self.resizable(False, False)
        self.celsius    = tk.StringVar(value="0")
        self.fahrenheit = tk.StringVar(value="32")
        self._build_ui()
        self.celsius.trace_add("write", self._from_c)
        self.fahrenheit.trace_add("write", self._from_f)

    def _build_ui(self):
        f = ttk.Frame(self, padding=20)
        f.pack()
        ttk.Label(f, text="°C").grid(row=0, column=1)
        ttk.Entry(f, textvariable=self.celsius,    width=10).grid(row=0, column=0)
        ttk.Label(f, text=" = ").grid(row=0, column=2)
        ttk.Entry(f, textvariable=self.fahrenheit, width=10).grid(row=0, column=3)
        ttk.Label(f, text="°F").grid(row=0, column=4)

    def _from_c(self, *_):
        try: self.fahrenheit.set(f"{float(self.celsius.get())*9/5+32:.2f}")
        except: pass

    def _from_f(self, *_):
        try: self.celsius.set(f"{(float(self.fahrenheit.get())-32)*5/9:.2f}")
        except: pass

TempConverter().mainloop()
💡 Class-Based Apps

Subclassing tk.Tk keeps your code organised and scales well as apps grow.

The Technology Stack

Tkinter is a binding — it exposes a Python API for the Tk GUI toolkit, written in C and controlled via the Tcl scripting language.

abstraction layers
  ┌─────────────────────────────────┐
  │     Your Python Application     │  ← You write this
  ├─────────────────────────────────┤
  │   tkinter  (Python module)      │  ← Python ↔ Tcl bridge
  ├─────────────────────────────────┤
  │   Tcl/Tk   (C library)          │  ← Actual GUI engine
  ├─────────────────────────────────┤
  │   OS windowing system           │  ← Win32 / Cocoa / X11
  └─────────────────────────────────┘

The Tk Instance

Every Tkinter application has exactly one tk.Tk() instance. It represents the connection to the Tk interpreter and creates the main (root) window.

⚠️ Only One Tk()

Never create more than one tk.Tk(). For additional windows use tk.Toplevel(). Multiple Tk() instances cause unpredictable behaviour.

Widget Hierarchy

Widgets form a parent-child tree. Every widget (except root) has a parent passed as the first positional argument.

hierarchy.py
root  = tk.Tk()                        # "."
frame = ttk.Frame(root)              # ".!frame"
label = ttk.Label(frame, text="Hi") # ".!frame.!label"

print(label.winfo_parent())          # ".!frame"
print(frame.winfo_children())        # [<Label .!frame.!label>]

The Event Loop

Tkinter is event-driven. After mainloop(), Tkinter continuously: waits for an event → dispatches to handlers → redraws changed widgets → repeat. Long-running code in a callback blocks and freezes the UI — see Threading.

tk vs ttk

tkinter (tk)
  • Original widgets since the 1990s
  • Configure every visual property directly
  • Consistent cross-platform look (not native)
  • import tkinter as tk
tkinter.ttk (ttk)
  • Introduced in Tk 8.5
  • Uses OS native appearance where possible
  • Extra widgets: Treeview, Notebook, Progressbar…
  • from tkinter import ttk
💡 Best Practice

Prefer ttk widgets whenever they exist. Fall back to tk for Canvas, Text, Listbox, and other widgets with no ttk equivalent.

ManagerBest ForModel
packSimple linear layoutsStack top/bottom/left/right
gridForms, tablesRow/column spreadsheet
placeAbsolute positioningx/y coords or fractions
⚠️ Never Mix Managers in the Same Container

Don't use both pack and grid for widgets sharing the same parent — it causes an infinite geometry loop and freeze.

pack()

widget.pack(side=TOP, fill=NONE, expand=False, anchor=CENTER, padx=0, pady=0)
OptionValuesDescription
sideTOP, BOTTOM, LEFT, RIGHTWhich side to pack against
fillNONE, X, Y, BOTHExpand to fill allocated space
expandTrue / FalseClaim extra space as window grows
anchorN S E W NE NW SE SW CENTERAlignment when there's spare space
padx / padyint or (int,int)External padding in pixels
sidebar_layout.py
container = ttk.Frame(root)
container.pack(fill=tk.BOTH, expand=True)

sidebar = ttk.Frame(container, width=120)
sidebar.pack(side=tk.LEFT, fill=tk.Y)

content = ttk.Frame(container)
content.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)

grid()

widget.grid(row=0, column=0, rowspan=1, columnspan=1, sticky="", padx=0, pady=0)
💡 columnconfigure / rowconfigure

Always call parent.columnconfigure(n, weight=1) on columns that should grow when the window is resized.

grid_form.py
root.columnconfigure(1, weight=1)

ttk.Label(root, text="Username:").grid(row=0, column=0, sticky="e", padx=8, pady=6)
ttk.Entry(root).grid            (row=0, column=1, sticky="ew", padx=8)

ttk.Label(root, text="Password:").grid(row=1, column=0, sticky="e", padx=8, pady=6)
ttk.Entry(root, show="*").grid     (row=1, column=1, sticky="ew", padx=8)

ttk.Button(root, text="Login").grid(row=2, column=0, columnspan=2, pady=12)

place()

widget.place(x=0, y=0, relx=0.0, rely=0.0, width=None, relwidth=None, anchor=NW)
place_center.py
# Center widget using relative coords
label.place(relx=0.5, rely=0.5, anchor=tk.CENTER)

Layout Recipes

App Skeleton (toolbar + content + statusbar)

app_skeleton.py
toolbar   = ttk.Frame(root, relief="raised")
toolbar.pack(side="top", fill="x")
content   = ttk.Frame(root)
content.pack(fill="both", expand=True)
statusbar = ttk.Label(root, text="Ready", relief="sunken")
statusbar.pack(side="bottom", fill="x")

bind() Basics

widget.bind(sequence, callback, add=None) → funcid
bind_basics.py
label.bind("<Button-1>", lambda e: print(f"Click at {e.x},{e.y}"))
root.bind("<Control-s>", lambda e: save())
root.bind("<Escape>",      lambda e: root.destroy())

The Event Object

AttributeTypeDescription
widgetWidgetThe widget that received the event
x, yintMouse position relative to widget
x_root, y_rootintMouse position relative to screen
numintMouse button number (1, 2, 3)
keysymstrKey name (e.g. "Return", "a", "F5")
charstrCharacter typed (may be empty)
stateintModifier key bitmask (Shift, Ctrl, Alt)
deltaintScroll amount (MouseWheel events)
width, heightintNew size (Configure events)

Mouse Events

SequenceFires when…
<Button-1>Left mouse button pressed
<Button-3>Right mouse button pressed
<ButtonRelease-1>Left button released
<Double-Button-1>Left button double-clicked
<B1-Motion>Mouse moved while left button held
<Motion>Mouse moved (any button or none)
<Enter> / <Leave>Mouse cursor entered/left widget
<MouseWheel>Mouse wheel scrolled (event.delta)

Keyboard Events

SequenceDescription
<Key>Any key pressed
<Return>Enter key
<Escape>Escape key
<Tab> / <BackSpace> / <Delete>Tab / Backspace / Delete
<Up> <Down> <Left> <Right>Arrow keys
<F1> … <F12>Function keys
<Control-s>Ctrl+S
<Control-Shift-Z>Ctrl+Shift+Z

Virtual Events

Virtual EventWhen Generated
<<Cut>> <<Copy>> <<Paste>>Clipboard operations
<<ComboboxSelected>>ttk.Combobox selection changed
<<NotebookTabChanged>>ttk.Notebook tab switched
<<TreeviewSelect>>ttk.Treeview row selected
<<ListboxSelect>>Listbox selection changed

after() — Timers

widget.after(ms, func=None, *args) → after_id
countdown.py
count = tk.IntVar(value=10)
label = ttk.Label(root, textvariable=count, font=("TkDefaultFont", 48))
label.pack(padx=40, pady=20)

def tick():
    n = count.get()
    if n > 0:
        count.set(n - 1)
        root.after(1000, tick)
    else:
        label.config(text="Done!")

root.after(1000, tick)

Variable Types

ClassHoldsDefault
StringVarstr""
IntVarint0
DoubleVarfloat0.0
BooleanVarboolFalse
variable_binding.py
slider_val = tk.DoubleVar()

# Scale widget writes to slider_val
ttk.Scale(root, variable=slider_val, from_=0, to=100).pack(fill="x", padx=20)

# Label automatically shows current value
ttk.Label(root, textvariable=slider_val).pack()

# Entry also linked — typing moves the slider
ttk.Entry(root, textvariable=slider_val).pack(pady=4)

trace_add() — Watch for Changes

var.trace_add(mode, callback) → trace_id

mode is one of "read", "write", or "unset". The callback receives (name, index, mode).

live_search.py
ITEMS = ["apple", "apricot", "banana", "blueberry", "cherry"]
query = tk.StringVar()

ttk.Entry(root, textvariable=query).pack(pady=8)
listbox = tk.Listbox(root, height=6)
listbox.pack(pady=4)

def filter_items(*_):
    q = query.get().lower()
    listbox.delete(0, tk.END)
    for item in ITEMS:
        if q in item:
            listbox.insert(tk.END, item)

query.trace_add("write", filter_items)
filter_items()

Widget-Variable Links

WidgetOptionVariable Type
Entry, Label, Buttontextvariable=StringVar
Checkbuttonvariable=BooleanVar or IntVar
Radiobuttonvariable=StringVar or IntVar
Scalevariable=IntVar or DoubleVar
Spinbox / Comboboxtextvariable=StringVar
tk.Labelttk.Label
label_demo.py
ttk.Label(root, text="Hello, World!").pack(pady=8)

tk.Label(root,
    text="Styled", font=("Helvetica", 16, "bold"),
    fg="white", bg="navy", padx=12, pady=6).pack(pady=8)

# Live clock with textvariable
time_var = tk.StringVar()
ttk.Label(root, textvariable=time_var, font=("TkFixedFont", 24)).pack(pady=20)

def tick():
    from datetime import datetime
    time_var.set(datetime.now().strftime("%H:%M:%S"))
    root.after(1000, tick)
tick()

Key Options

OptionDescription
textThe string to display
textvariableStringVar — live updates
imagePhotoImage to display
compoundLEFT/RIGHT/TOP/BOTTOM — image + text arrangement
fontFont tuple: ("family", size, "bold italic")
fg / bgText/background color (tk only)
anchorN/S/E/W/CENTER — alignment
wraplengthWrap text at this pixel width
justifyLEFT/RIGHT/CENTER for multi-line text
relief / bdBorder style and width
⚠️ Image References

Always store PhotoImage objects as instance attributes (self.img = ... or label.image = ...). Python garbage-collects unreferenced images and the widget goes blank.

tk.Buttonttk.Button
button_demo.py
ttk.Button(root, text="Click Me", command=on_click).pack(pady=8)

# Loading state pattern
def on_submit():
    btn.state(["disabled"])
    btn.config(text="Working…")
    def done():
        btn.config(text="Submit")
        btn.state(["!disabled"])
    root.after(2000, done)   # simulate work

btn = ttk.Button(root, text="Submit", command=on_submit)
btn.pack(pady=12)

Key Options

OptionDescription
textButton label text
commandFunction called on click (no arguments passed)
textvariableStringVar providing the label
image / compoundImage and text arrangement
statenormal/active/disabled (tk); use state() for ttk
widthWidth in characters

Methods

button.invoke() — Simulate a button click programmatically
button.state(["disabled"]) — ttk state flags
button.state(["!disabled"]) — ttk remove flag (prefix with !)
tk.Entryttk.Entry
entry_demo.py
username = tk.StringVar()
entry = ttk.Entry(root, textvariable=username, width=30)
entry.pack(pady=8)
entry.focus_set()   # give keyboard focus

# Methods
entry.insert(0, "Hello")     # insert at start
entry.insert(tk.END, " World") # append
value = entry.get()             # "Hello World"
entry.delete(0, tk.END)         # clear

Validation — Integers Only

validate_int.py
def only_integers(new_value):
    return new_value == "" or new_value.lstrip("-").isdigit()

vcmd = root.register(only_integers)
ttk.Entry(root,
    validate="key",
    validatecommand=(vcmd, "%P")).pack(padx=20, pady=20)

Validation Substitutions

CodeMeaning
%PProposed value if change is allowed
%sCurrent value before the change
%SText being inserted or deleted
%dAction: 1=insert, 0=delete, -1=other
%iIndex of the change
%VTrigger: key / focusin / focusout / forced

Password Field

password.py
pw = tk.StringVar()
show = tk.BooleanVar(value=False)
pw_entry = ttk.Entry(root, textvariable=pw, show="●")
pw_entry.pack(pady=8)

ttk.Checkbutton(root, text="Show", variable=show,
    command=lambda: pw_entry.config(show="" if show.get() else "●")).pack()
tk.Text

Basic Usage

text_basic.py
text = tk.Text(root, width=50, height=10, wrap=tk.WORD, undo=True)
text.pack(padx=8, pady=8)
text.insert("1.0", "Hello, World!
Second line.")
content = text.get("1.0", tk.END).rstrip("\n")

Index Notation

IndexMeaning
"1.0"Line 1, column 0 (the very beginning)
"2.5"Line 2, column 5
"end" or tk.ENDOne past the last character
"insert"Current insertion cursor
"sel.first" / "sel.last"Start/end of selection
"1.0 lineend"End of line 1
"1.0 + 3c"3 characters after 1.0

Tags (Formatting)

text_tags.py
text.tag_configure("bold",     font=("TkDefaultFont", 11, "bold"))
text.tag_configure("red",      foreground="red")
text.tag_configure("highlight",background="yellow")
text.tag_configure("link",     foreground="blue", underline=True)

text.insert(tk.END, "Bold text", "bold")
text.tag_bind("link", "<Button-1>", lambda e: print("clicked!"))

Scrolled Text

scrolled_text.py
frame = ttk.Frame(root)
frame.pack(fill="both", expand=True)
frame.rowconfigure(0, weight=1); frame.columnconfigure(0, weight=1)

text = tk.Text(frame, wrap=tk.WORD)
sb   = ttk.Scrollbar(frame, orient="vertical", command=text.yview)
text.configure(yscrollcommand=sb.set)
text.grid(row=0, column=0, sticky="nsew")
sb.grid  (row=0, column=1, sticky="ns")
tk.Framettk.Frametk.LabelFramettk.LabelFrame

Frame — Layout Container

frame_layout.py
root.rowconfigure(0, weight=1); root.columnconfigure(0, weight=1)
main = ttk.Frame(root); main.grid(row=0, column=0, sticky="nsew")
main.rowconfigure(1, weight=1); main.columnconfigure(1, weight=1)

header  = ttk.Frame(main, height=48)
sidebar = ttk.Frame(main, width=200)
content = ttk.Frame(main)

header.grid (row=0, column=0, columnspan=2, sticky="ew")
sidebar.grid(row=1, column=0, sticky="ns")
content.grid(row=1, column=1, sticky="nsew")
sidebar.grid_propagate(False)  # freeze sidebar width

LabelFrame — Grouped Controls

labelframe.py
group = ttk.LabelFrame(root, text="Connection Settings", padding=12)
group.pack(padx=16, pady=16, fill="x")
group.columnconfigure(1, weight=1)

ttk.Label(group, text="Host:").grid(row=0, column=0, sticky="e")
ttk.Entry(group).grid            (row=0, column=1, sticky="ew", padx=4)
ttk.Label(group, text="Port:").grid(row=1, column=0, sticky="e")
ttk.Entry(group, width=8).grid  (row=1, column=1, sticky="w",  padx=4)

pack_propagate

fixed_size_frame.py
sidebar = ttk.Frame(root, width=200)
sidebar.pack_propagate(False)  # don't shrink to children
sidebar.pack(side="left", fill="y")
tk.Canvas

Drawing Methods

canvas_shapes.py
canvas = tk.Canvas(root, width=400, height=300, bg="white", highlightthickness=0)
canvas.pack()

rect = canvas.create_rectangle(50, 50, 150, 100, fill="skyblue", outline="navy")
oval = canvas.create_oval      (200, 50, 300, 150, fill="tomato")
line = canvas.create_line      (0, 200, 400, 200, fill="gray", dash=(4, 2))
text = canvas.create_text      (200, 250, text="Hello Canvas", font=("Arial", 14))

All Draw Methods

MethodKey Options
create_rectangle(x1,y1,x2,y2)fill, outline, width
create_oval(x1,y1,x2,y2)fill, outline
create_line(x1,y1,x2,y2,...)fill, width, dash, arrow, smooth
create_polygon(x1,y1,...)fill, outline, smooth
create_arc(x1,y1,x2,y2)start, extent, style (ARC/CHORD/PIESLICE)
create_text(x,y)text, font, fill, anchor, justify
create_image(x,y)image (PhotoImage), anchor
create_window(x,y)window (any widget!), anchor

Modifying Items

canvas.itemconfig(item_id_or_tag, **options)
canvas.move(item, dx, dy)
canvas.coords(item, *new_coords) → list
canvas.delete(item_or_tag)
canvas.lift(item) / canvas.lower(item)

Draggable Shapes

drag_shapes.py
canvas.create_oval(40,40,100,100, fill="dodgerblue", tags="drag")
_d = {"x":0,"y":0,"item":None}

def start(e):
    _d["item"] = canvas.find_closest(e.x, e.y)[0]
    _d["x"], _d["y"] = e.x, e.y

def drag(e):
    canvas.move(_d["item"], e.x-_d["x"], e.y-_d["y"])
    _d["x"], _d["y"] = e.x, e.y

canvas.tag_bind("drag", "<Button-1>", start)
canvas.tag_bind("drag", "<B1-Motion>", drag)
tk.Listbox

Basic Usage

listbox_demo.py
lb = tk.Listbox(root, height=6, selectmode=tk.EXTENDED)
lb.pack(padx=10, pady=10)

for fruit in ["Apple", "Banana", "Cherry", "Durian"]:
    lb.insert(tk.END, fruit)

lb.bind("<<ListboxSelect>>",
    lambda e: print([lb.get(i) for i in lb.curselection()]))

Selection Modes

ModeBehavior
BROWSESingle selection; click drags (default)
SINGLESingle selection; no drag
MULTIPLEToggle multiple with click
EXTENDEDRange select with Shift+click, Ctrl+click

Key Methods

lb.insert(index, *elements) — Add items (END, 0, or int)
lb.delete(first, last=None)
lb.get(first, last=None) → str or tuple
lb.curselection() → tuple of ints
lb.selection_set(first, last=None)
lb.size() → int
lb.see(index) — Scroll to make item visible
lb.itemconfig(index, fg=, bg=) — Color individual items
tk.Scrollbarttk.Scrollbar

Scrollbars link to a scrollable widget through a two-way command interface.

scrollbar_link.py
text = tk.Text(frame, wrap="word")
vbar = ttk.Scrollbar(frame, orient="vertical")

# TWO-WAY link
text.configure(yscrollcommand=vbar.set)  # widget tells scrollbar position
vbar.configure(command=text.yview)       # scrollbar tells widget to scroll

text.grid(row=0, column=0, sticky="nsew")
vbar.grid(row=0, column=1, sticky="ns")
frame.rowconfigure(0, weight=1); frame.columnconfigure(0, weight=1)

Compatible widgets: Text Canvas Listbox Entry ttk.Treeview

tk.Checkbuttonttk.Checkbutton
checkbutton.py
notifications = tk.BooleanVar(value=True)
dark_mode     = tk.BooleanVar(value=False)

ttk.Checkbutton(root, text="Enable notifications",
    variable=notifications).pack(anchor="w", padx=12, pady=4)
ttk.Checkbutton(root, text="Dark mode",
    variable=dark_mode,
    command=lambda: print("Dark:", dark_mode.get())).pack(anchor="w", padx=12)
ttk.Button(root, text="Show values",
    command=lambda: print(notifications.get(), dark_mode.get())).pack(pady=8)

Key Options

OptionDescription
variableBooleanVar (or IntVar) tracking state
onvalue / offvalueValues for checked/unchecked (default True/False)
commandCallback on toggle
text / textvariableLabel text
tk.Radiobuttonttk.Radiobutton

All Radiobuttons sharing the same variable form a group — only one selectable at a time.

radiobutton.py
size = tk.StringVar(value="M")

for label, value in [("Small","S"),("Medium","M"),("Large","L")]:
    ttk.Radiobutton(root, text=label, variable=size, value=value).pack(anchor="w", padx=20)

ttk.Button(root, text="Show",
    command=lambda: print("Size:", size.get())).pack(pady=8)

Key Options

OptionDescription
variableStringVar or IntVar shared by the group
valueThe value this button sets when selected
commandCallback when this button is selected
indicatoron0 = button-style (no radio dot) — tk only
tk.Scalettk.Scale
rgb_mixer.py
r, g, b = tk.IntVar(), tk.IntVar(), tk.IntVar()
preview = tk.Label(root, width=20, height=3)
preview.pack(pady=8)

def update(*_):
    preview.config(bg=f"#{r.get():02x}{g.get():02x}{b.get():02x}")

for label, var, fg in [("R",r,"red"),("G",g,"green"),("B",b,"blue")]:
    f = tk.Frame(root)
    f.pack(fill="x", padx=10)
    tk.Label(f, text=label, fg=fg, width=2).pack(side="left")
    tk.Scale(f, variable=var, from_=0, to=255,
             orient="horizontal", command=update,
             showvalue=False).pack(side="left", fill="x", expand=True)

Key Options

OptionDescription
from_ / toMin/max value (from_ has trailing underscore — "from" is a keyword)
orientHORIZONTAL or VERTICAL
variableIntVar or DoubleVar
resolutionSnap increment
tickintervalSpacing between tick labels (tk only)
commandCallback called with new value string
tk.Spinboxttk.Spinbox
spinbox_demo.py
quantity = tk.IntVar(value=1)
ttk.Spinbox(root, from_=1, to=99, textvariable=quantity, width=6).pack(pady=8)

day = tk.StringVar(value="Monday")
ttk.Spinbox(root,
    values=("Monday","Tuesday","Wednesday","Thursday","Friday"),
    textvariable=day, width=12, state="readonly").pack(pady=4)

Key Options

OptionDescription
from_ / to / incrementNumeric range and step
valuesTuple of strings to cycle through
wrapTrue = cycle back to start/end
formatprintf-style format, e.g. "%.2f"
statenormal / readonly / disabled
tk.Menu

Menu Bar

menubar.py
menubar = tk.Menu(root)
root.config(menu=menubar)

file_menu = tk.Menu(menubar, tearoff=0)
menubar.add_cascade(label="File", menu=file_menu)

file_menu.add_command  (label="New",  command=new_file,  accelerator="Ctrl+N")
file_menu.add_command  (label="Open…", command=open_file, accelerator="Ctrl+O")
file_menu.add_separator()
file_menu.add_command  (label="Exit",  command=root.destroy)

edit_menu = tk.Menu(menubar, tearoff=0)
menubar.add_cascade(label="Edit", menu=edit_menu)
word_wrap = tk.BooleanVar(value=True)
edit_menu.add_checkbutton(label="Word Wrap", variable=word_wrap)

Context Menu

context_menu.py
ctx = tk.Menu(root, tearoff=0)
ctx.add_command(label="Cut",   command=lambda: text.event_generate("<<Cut>>"))
ctx.add_command(label="Copy",  command=lambda: text.event_generate("<<Copy>>"))
ctx.add_command(label="Paste", command=lambda: text.event_generate("<<Paste>>"))

text.bind("<Button-3>", lambda e: ctx.tk_popup(e.x_root, e.y_root))

Item Types

MethodDescription
add_command()Normal clickable item
add_separator()Horizontal line
add_checkbutton()Toggle item with variable=
add_radiobutton()Radio item with variable= and value=
add_cascade()Submenu (menu= another Menu)
tk.Toplevel

Basic Window

toplevel.py
def open_window():
    win = tk.Toplevel(root)
    win.title("Secondary Window")
    win.geometry("300x200")
    ttk.Button(win, text="Close", command=win.destroy).pack(pady=20)

Modal Dialog Pattern

modal_dialog.py
class AskNameDialog:
    def __init__(self, parent):
        self.result = None
        win = tk.Toplevel(parent)
        win.grab_set()          # capture all input events
        win.transient(parent)   # stay above parent
        win.title("Enter Name")

        name = tk.StringVar()
        ttk.Label(win, text="Name:").pack(padx=16, pady=(16,4))
        ttk.Entry(win, textvariable=name).pack(padx=16)

        def ok():
            self.result = name.get()
            win.destroy()

        ttk.Button(win, text="OK", command=ok).pack(pady=12)
        parent.wait_window(win)  # block until closed

dlg = AskNameDialog(root)
print(dlg.result)  # available after dialog closes

Close Protocol

confirm_close.py
from tkinter import messagebox

def on_close():
    if messagebox.askokcancel("Quit", "Do you want to quit?"):
        root.destroy()

root.protocol("WM_DELETE_WINDOW", on_close)

Importing

import_ttk.py
import tkinter as tk
from tkinter import ttk

tk vs ttk Key Differences

Featuretk widgetsttk widgets
AppearanceCustom cross-platformNative OS look
Color/font configbg=, fg=, font= directlyVia ttk.Style
State systemstate="disabled"widget.state(["disabled"])
Extra widgetsNoneTreeview, Notebook, Progressbar, Combobox, Separator, Sizegrip

Available Themes

themes.py
style = ttk.Style()
print(style.theme_names())   # list available
print(style.theme_use())    # current theme
style.theme_use("clam")     # switch theme

State System

ttk_state.py
btn.state(["disabled"])          # set flag
btn.state(["!disabled"])         # clear flag (! = negate)
print(btn.instate(["disabled"])) # True/False
print(btn.state())                # e.g. ('focus',)

All TTK Widgets

configure()

style_basic.py
style = ttk.Style(root)
style.theme_use("clam")

style.configure("TButton",
    font=("Helvetica", 11, "bold"),
    padding=(10, 6),
    background="#2563eb",
    foreground="white")

map() — State-Based Styles

style_map.py
style.map("TButton",
    background=[
        ("disabled", "#9ca3af"),
        ("active",   "#1d4ed8"),
        ("pressed",  "#1e3a8a"),
        ("!disabled","#2563eb"),
    ],
    foreground=[("disabled","#e5e7eb"),("!disabled","white")],
)

Named (Custom) Styles

named_styles.py
# Name must end with .TClassName
style.configure("Danger.TButton", background="#dc2626", foreground="white")
style.configure("Success.TButton",background="#16a34a", foreground="white")

ttk.Button(root, text="Delete",  style="Danger.TButton").pack()
ttk.Button(root, text="Confirm", style="Success.TButton").pack()

Debugging

debug.py
print(style.configure("TButton"))          # current settings
print(style.lookup("TButton", "background"))# resolved value
print(style.layout("TButton"))             # element layout
ttk.Notebook
notebook_demo.py
nb = ttk.Notebook(root)
nb.pack(fill="both", expand=True, padx=8, pady=8)

tab1 = ttk.Frame(nb, padding=12)
tab2 = ttk.Frame(nb, padding=12)

nb.add(tab1, text="General")
nb.add(tab2, text="Advanced")

ttk.Label(tab1, text="General settings").pack()
ttk.Label(tab2, text="Advanced options").pack()

Methods

nb.add(child, text=, state=, image=)
nb.insert(pos, child, **opts)
nb.forget(tab_id) — Remove tab
nb.hide(tab_id) — Hide without removing
nb.select(tab_id=None) → tab_id — Get or switch
nb.tab(tab_id, option=None) — Get/set tab options
nb.enable_traversal() — Ctrl+Tab keyboard navigation

Tab Change Event

tab_events.py
def on_change(event):
    sel  = nb.select()
    text = nb.tab(sel, "text")
    print(f"Switched to: {text}")

nb.bind("<<NotebookTabChanged>>", on_change)
ttk.Treeview

Table Mode

treeview_table.py
columns = ("name", "age", "city")
tree = ttk.Treeview(root, columns=columns, show="headings", height=8)

for col in columns:
    tree.heading(col, text=col.title())

for row in [("Alice",30,"NYC"),("Bob",24,"London"),("Carol",35,"Tokyo")]:
    tree.insert("", tk.END, values=row)

tree.pack(fill="both", expand=True)

Tree Mode

treeview_tree.py
tree = ttk.Treeview(root)
food   = tree.insert("", tk.END, text="Food",  open=True)
fruit  = tree.insert(food, tk.END, text="Fruit")
tree.insert(fruit, tk.END, text="Apple")
tree.insert(fruit, tk.END, text="Banana")

Sortable Columns

sort_columns.py
def sort_col(tree, col, reverse):
    data = [(tree.set(iid, col), iid) for iid in tree.get_children()]
    data.sort(reverse=reverse)
    for i, (_, iid) in enumerate(data):
        tree.move(iid, "", i)
    tree.heading(col, command=lambda: sort_col(tree, col, not reverse))

for col in columns:
    tree.heading(col, text=col.title(),
                 command=lambda c=col: sort_col(tree, c, False))
ttk.Progressbar
progressbar_demo.py
progress = tk.IntVar(value=0)

# Determinate — shows exact %
bar = ttk.Progressbar(root, variable=progress, maximum=100, length=300)
bar.pack(pady=20)

# Indeterminate — animated "working" bar
busy = ttk.Progressbar(root, mode="indeterminate", length=300)
busy.pack(pady=8)
busy.start(15)   # ms between steps

With Background Thread

progress_thread.py
import threading, time

progress = tk.DoubleVar()
bar = ttk.Progressbar(root, variable=progress, maximum=100, length=300)
bar.pack(pady=20)

def work():
    for i in range(101):
        time.sleep(0.05)
        root.after(0, progress.set, i)

threading.Thread(target=work, daemon=True).start()
ttk.Combobox
combobox_demo.py
country = tk.StringVar()
combo = ttk.Combobox(root,
    textvariable=country,
    values=["Canada", "France", "Germany", "Japan", "UK", "USA"],
    state="readonly",
    width=18)
combo.pack(pady=12)
combo.set("USA")

combo.bind("<<ComboboxSelected>>",
    lambda e: print("Selected:", country.get()))

Key Options

OptionDescription
valuesList/tuple of dropdown options
state"normal" (editable), "readonly", "disabled"
heightMax rows shown in dropdown
postcommandCallback fired before dropdown opens (to refresh values)
dynamic_combo.py
# Refresh values just before dropdown opens
def refresh():
    combo["values"] = get_current_options()

combo.configure(postcommand=refresh)

messagebox

messagebox_demo.py
from tkinter import messagebox

messagebox.showinfo   ("Done",    "File saved.")
messagebox.showwarning("Warning", "Low disk space.")
messagebox.showerror  ("Error",   "File not found.")

if messagebox.askyesno("Confirm", "Delete this item?"):
    delete_item()

if messagebox.askokcancel("Quit", "Unsaved changes. Quit anyway?"):
    root.destroy()

filedialog

filedialog_demo.py
from tkinter import filedialog

path = filedialog.askopenfilename(
    title="Open File",
    filetypes=[("Text files","*.txt"),("All files","*.*")])
if path: print(f"Opening: {path}")

save_path = filedialog.asksaveasfilename(defaultextension=".txt")
folder    = filedialog.askdirectory(title="Select Folder")

colorchooser

colorchooser_demo.py
from tkinter import colorchooser

# Returns ((R,G,B), "#rrggbb") or (None, None) if cancelled
rgb, hex_col = colorchooser.askcolor(color="#ff0000", title="Pick Colour")
if hex_col:
    label.config(bg=hex_col)

simpledialog

simpledialog_demo.py
from tkinter import simpledialog

name  = simpledialog.askstring ("Name",  "Enter your name:")
age   = simpledialog.askinteger("Age",   "Enter your age:", minvalue=0, maxvalue=150)
price = simpledialog.askfloat  ("Price", "Enter price:",   minvalue=0.0)
🚫 Never call Tkinter from a non-main thread

Tkinter is not thread-safe. Only the main thread should call Tk methods. Background threads must send results back via root.after(0, callback, result) or a queue.Queue.

Background Thread + after()

background_thread.py
import threading, time
import tkinter as tk
from tkinter import ttk

root = tk.Tk()
status = ttk.Label(root, text="Idle")
status.pack(pady=8)
bar = ttk.Progressbar(root, mode="indeterminate", length=200)
bar.pack(pady=4)

def do_work():
    time.sleep(3)               # your long-running task here
    result = "42"
    root.after(0, on_done, result)   # safe callback to main thread

def on_done(result):
    bar.stop()
    status.config(text=f"Result: {result}")
    btn.state(["!disabled"])

def start():
    btn.state(["disabled"])
    status.config(text="Working…")
    bar.start(10)
    threading.Thread(target=do_work, daemon=True).start()

btn = ttk.Button(root, text="Start", command=start)
btn.pack(pady=8)
root.mainloop()

Thread-Safe Queue Pattern

queue_pattern.py
import threading, queue, time

msg_queue = queue.Queue()

def worker():
    for i in range(10):
        time.sleep(0.5)
        msg_queue.put(f"Step {i+1}/10
")
    msg_queue.put(None)   # sentinel: done

def poll():
    while True:
        try:
            msg = msg_queue.get_nowait()
        except queue.Empty:
            break
        if msg is None: return
        log.config(state="normal")
        log.insert("end", msg)
        log.config(state="disabled")
    root.after(100, poll)   # check again in 100ms

threading.Thread(target=worker, daemon=True).start()
root.after(100, poll)

PhotoImage (Built-in)

Supports GIF, PGM, PPM, and PNG natively. For JPEG and other formats use Pillow.

photoimage.py
img = tk.PhotoImage(file="photo.png")

label = ttk.Label(root, image=img)
label.image = img   # ← MUST keep reference or image vanishes!
label.pack()

# Resize (integer factors only)
small = img.subsample(2, 2)   # ÷2
big   = img.zoom(3, 3)         # ×3
⚠️ The Garbage Collection Gotcha

Always assign your PhotoImage to self.img or widget.image. Local variables get garbage-collected before Tk renders the image.

Pillow for JPEG & More

terminal
pip install Pillow
pillow_demo.py
from PIL import Image, ImageTk

pil_img = Image.open("photo.jpg")
pil_img = pil_img.resize((200, 200), Image.LANCZOS)
tk_img  = ImageTk.PhotoImage(pil_img)

label = tk.Label(root, image=tk_img)
label.image = tk_img   # keep reference
label.pack()

Images on Canvas

canvas_image.py
canvas = tk.Canvas(root, width=400, height=300)
canvas.pack()
img = tk.PhotoImage(file="bg.png")
canvas.image = img
canvas.create_image(0, 0, image=img, anchor="nw")

Specifying Fonts

font_formats.py
from tkinter import font

# Tuple format (family, size, style...)
tk.Label(root, font=("Helvetica", 14, "bold italic"))

# Font object (most flexible)
my_font = font.Font(family="Helvetica", size=14, weight="bold")
tk.Label(root, font=my_font)

Named System Fonts

NameUse
TkDefaultFontDefault for most widgets
TkTextFontEntry, Text widgets
TkFixedFontMonospace / code
TkMenuFontMenu items
TkHeadingFontColumn headings
TkCaptionFontWindow titles

Font Object Methods

f.actual(option=None) → dict — Actual rendered properties
f.configure(**opts) — Change font (updates all linked widgets)
f.measure(text) → int — Width in pixels
f.metrics() → dict — ascent, descent, fixed, linespace
change_default_font.py
# Change default font for ALL widgets at once
font.nametofont("TkDefaultFont").configure(family="Arial", size=11)
font.nametofont("TkTextFont").configure(family="Consolas", size=11)

# List all available font families
print(sorted(font.families()))

MVC Application Structure

mvc_pattern.py
class Model:
    def __init__(self): self.items = []
    def add(self, item): self.items.append(item)

class View(ttk.Frame):
    def __init__(self, parent):
        super().__init__(parent, padding=12)
        self.entry_var = tk.StringVar()
        ttk.Entry(self, textvariable=self.entry_var).pack(fill="x")
        self.add_btn = ttk.Button(self, text="Add")
        self.add_btn.pack(pady=4)
        self.lb = tk.Listbox(self)
        self.lb.pack(fill="both", expand=True)
    def refresh(self, items):
        self.lb.delete(0, tk.END)
        for item in items: self.lb.insert(tk.END, item)

class Controller:
    def __init__(self, model, view):
        self.m, self.v = model, view
        view.add_btn.configure(command=self._add)
    def _add(self):
        t = self.v.entry_var.get().strip()
        if t: self.m.add(t); self.v.entry_var.set(""); self.v.refresh(self.m.items)

Scrollable Frame

scrollable_frame.py
class ScrollableFrame(ttk.Frame):
    def __init__(self, parent, **kw):
        super().__init__(parent, **kw)
        canvas = tk.Canvas(self, highlightthickness=0)
        sb = ttk.Scrollbar(self, orient="vertical", command=canvas.yview)
        canvas.configure(yscrollcommand=sb.set)
        sb.pack(side="right", fill="y")
        canvas.pack(side="left", fill="both", expand=True)
        self.inner = ttk.Frame(canvas)
        wid = canvas.create_window((0,0), window=self.inner, anchor="nw")
        self.inner.bind("<Configure>",
            lambda e: canvas.configure(scrollregion=canvas.bbox("all")))
        canvas.bind("<Configure>",
            lambda e: canvas.itemconfig(wid, width=e.width))
        canvas.bind("<MouseWheel>",
            lambda e: canvas.yview_scroll(-1*(e.delta//120), "units"))

Tooltip

tooltip.py
class Tooltip:
    def __init__(self, widget, text):
        self.widget = widget
        self.text   = text
        self.tip    = None
        widget.bind("<Enter>", self._show)
        widget.bind("<Leave>", self._hide)

    def _show(self, event):
        x = self.widget.winfo_rootx() + 20
        y = self.widget.winfo_rooty() + self.widget.winfo_height() + 4
        self.tip = tw = tk.Toplevel(self.widget)
        tw.wm_overrideredirect(True)
        tw.wm_geometry(f"+{x}+{y}")
        tk.Label(tw, text=self.text, bg="#ffffe0",
                relief="solid", bd=1, padx=4).pack()

    def _hide(self, event):
        if self.tip: self.tip.destroy(); self.tip = None

Tooltip(save_btn, "Save the current file (Ctrl+S)")

Center Window on Screen

center_window.py
def center_window(win, width=400, height=300):
    win.update_idletasks()
    sw = win.winfo_screenwidth()
    sh = win.winfo_screenheight()
    x = (sw - width)  // 2
    y = (sh - height) // 2
    win.geometry(f"{width}x{height}+{x}+{y}")

center_window(root, 600, 400)

Debounced Entry (Search-as-you-type)

debounce.py
_after_id = None

def on_key(*_):
    global _after_id
    if _after_id:
        root.after_cancel(_after_id)
    _after_id = root.after(300, do_search)   # 300ms delay

def do_search():
    query = search_var.get()
    # perform search...

search_var.trace_add("write", on_key)