Tkinter Complete Guide: From Zero to Master
A gentle, thorough journey into Python's built-in GUI toolkit — no prior experience needed.
Let me ask you something: Have you ever written a powerful Python script — maybe one that organizes your files, crunches numbers, or scrapes data — only to realize it runs in a dull terminal window? No buttons. No menus. No colors. Just lines of text? What if you could click a button to run it? Drag a file into a box? See results update in real time? That’s the magic of Tkinter. It turns your silent scripts into living, breathing applications. And the best part? You don’t need to learn a new language. You just use Python… but now, with a face.
What Is Tkinter? The Simple Truth
Tkinter isn't some mysterious framework you install. It's not a trendy JavaScript library. It's Python's official, built-in way to create graphical user interfaces using the Tk toolkit — a decades-old, rock-solid GUI system originally made for Tcl. But here’s the beautiful twist: you write pure Python. No Tcl. No complex syntax. Just clean, readable code that creates windows, buttons, and text boxes. Think of Tkinter as Python’s hands — it lets your logic reach out and touch the real world.
import tkinter as tk
root = tk.Tk()
root.title("My First Window")
label = tk.Label(root, text="Hello from Tkinter!")
label.pack()
root.mainloop()
This tiny script? It’s your first desktop app. Run it. Watch a window appear. That’s Tkinter doing its job — quietly, reliably, without drama.
Why Tkinter? Why Not PyQt, Electron, or Flutter?
Great question. There are flashier tools out there — Qt for professional apps, Electron for web-like desktop software, Flutter for mobile-first design. But Tkinter wins on three things: simplicity, speed, and availability. It comes with Python. No pip install
. No dependencies. No bloated engines. It’s perfect for small tools, internal utilities, teaching, prototypes, and automation. If you're building a config editor for your own scripts, a data input form for your team, or a learning tool for students — Tkinter is the quiet hero that gets the job done without asking for permission.
How Does Tkinter Actually Work Under the Hood?
Tkinter acts like a bridge between Python and the native operating system’s GUI engine. When you call tk.Tk()
, Python talks to Tk (written in C), which then asks Windows, macOS, or Linux to draw a window. Every widget — button, label, entry — is actually a native OS control wrapped in Python objects. That’s why Tkinter apps feel fast and lightweight. They’re not running inside a browser or virtual machine. They’re talking directly to your computer’s interface layer. That’s power.
Your First Tkinter Window: The “Hello World” That Matters
This isn’t just “Hello World.” This is your first real desktop application. The magic happens in three steps: create a root window, add a widget (like a label), then start the event loop with mainloop()
. Without mainloop()
, the window opens and vanishes instantly — because Python finishes executing and exits. But with it? Python waits. It listens. It watches for clicks, keystrokes, drags. Your program becomes alive.
import tkinter as tk
root = tk.Tk() # Creates the main window
root.title("Welcome!") # Sets the window title
root.geometry("400x200") # Sets size: width x height
label = tk.Label(root, text="You did it!") # Creates a text label
label.pack(pady=50) # Places it in the center
root.mainloop() # Starts the event loop — THIS IS CRUCIAL
What Does mainloop()
Really Do? (And Why Can’t You Skip It?)
Imagine you’re waiting for a friend at a café. You sit down, order coffee, and then… leave immediately. You’d never see them. mainloop()
is like staying seated. It starts an infinite loop that constantly checks: “Did someone click? Press a key? Resize the window?” It keeps your program alive until you close the window. Without it, your GUI is like a ghost — visible for a millisecond, then gone. Don’t skip it. Treat it like the heartbeat of your app.
The Three Layout Managers: pack()
, grid()
, and place()
Tkinter gives you three ways to arrange widgets. Each has strengths. pack()
stacks things like cards — simple but limited. grid()
arranges them like a spreadsheet — precise and scalable. place()
lets you position pixels exactly — powerful but fragile when resizing. Most beginners start with pack()
, then graduate to grid()
for forms and tables. Use place()
only for special cases like overlays.
Using pack()
: The Simplest Way to Arrange Widgets
With pack()
, widgets stack vertically or horizontally. It’s automatic. You don’t need coordinates. Just call pack()
after creating each widget, and Tkinter figures out where to put it. Great for quick layouts — like a column of buttons or labels. But be careful: once packed, you can’t easily move a widget without re-packing everything.
import tkinter as tk
root = tk.Tk()
root.title("Pack Examples")
tk.Label(root, text="Top").pack()
tk.Label(root, text="Middle").pack()
tk.Label(root, text="Bottom").pack()
root.mainloop()
Using grid()
: Building Forms Like a Spreadsheet
If you’ve ever filled out a form online, you’ve seen grid()
in action. Rows and columns. Labels on the left, inputs on the right. Perfect for login screens, settings panels, data entry forms. Unlike pack()
, you specify exact positions with row
and column
. You can even span cells with columnspan
or rowspan
.
import tkinter as tk
root = tk.Tk()
root.title("Form with Grid")
tk.Label(root, text="Username:").grid(row=0, column=0, sticky="e", padx=5, pady=5)
tk.Entry(root).grid(row=0, column=1, padx=5, pady=5)
tk.Label(root, text="Password:").grid(row=1, column=0, sticky="e", padx=5, pady=5)
tk.Entry(root, show="*").grid(row=1, column=1, padx=5, pady=5)
tk.Button(root, text="Login").grid(row=2, column=0, columnspan=2, pady=10)
root.mainloop()
Using place()
: Absolute Positioning — Use With Care
place()
lets you set exact pixel coordinates. Want a button 100 pixels from the top and 50 from the left? Done. But here’s the catch: if the user resizes the window, your widgets stay fixed. They won’t adjust. So use this only for static overlays, popups, or custom designs where you control the window size. For most apps, stick with pack()
or grid()
.
import tkinter as tk
root = tk.Tk()
root.title("Place Example")
root.geometry("300x200")
tk.Label(root, text="Top Left").place(x=10, y=10)
tk.Label(root, text="Center").place(relx=0.5, rely=0.5, anchor="center")
tk.Label(root, text="Bottom Right").place(relx=1, rely=1, anchor="se")
root.mainloop()
Creating Buttons That Actually Do Something
A button without a function is just decoration. To make a button do something, you assign a function to its command
parameter. When clicked, that function runs. It’s that simple. You don’t need event listeners or callbacks — just pass the function name. No parentheses. No extra syntax. Pure Python elegance.
import tkinter as tk
def say_hello():
print("Button was clicked!")
root = tk.Tk()
root.title("Clickable Button")
button = tk.Button(root, text="Click Me!", command=say_hello)
button.pack(pady=50)
root.mainloop()
Passing Arguments to Button Functions Using Lambda
Sometimes you want a button to say “Hello Alice” or “Open file X.” But functions usually don’t take arguments in command
. Enter lambda
: a tiny anonymous function that wraps your call. It’s like saying: “When clicked, run this one-liner.” Perfect for dynamic behavior — like opening different files or changing messages based on context.
import tkinter as tk
def greet(name):
print(f"Hello, {name}!")
root = tk.Tk()
root.title("Lambda Buttons")
tk.Button(root, text="Greet Alice", command=lambda: greet("Alice")).pack(pady=5)
tk.Button(root, text="Greet Bob", command=lambda: greet("Bob")).pack(pady=5)
root.mainloop()
Updating Widget Text Dynamically: The Label That Changes
Labels aren’t static. You can change their text anytime using the config()
method. This is how you build live feedback — counters, status messages, progress indicators. The key is keeping a reference to the widget so you can modify it later. In this example, we update the label each time the button is pressed.
import tkinter as tk
count = 0
def increment():
global count
count += 1
label.config(text=f"Count: {count}") # Update the label
root = tk.Tk()
root.title("Dynamic Label")
label = tk.Label(root, text="Count: 0", font=("Arial", 16))
label.pack(pady=30)
tk.Button(root, text="Increment", command=increment).pack()
root.mainloop()
Input Fields: How Entry
Widgets Capture User Text
The Entry
widget is your text box. Users type into it. But how do you read what they typed? You call .get()
on the Entry object. This returns the current string value. Combine it with a button and you have a full input form. This is the foundation of almost every desktop app: collect data → process it → show results.
import tkinter as tk
def show_input():
user_text = entry.get() # Get the text from the Entry widget
result_label.config(text=f"You typed: {user_text}")
root = tk.Tk()
root.title("Text Input")
entry = tk.Entry(root, width=30)
entry.pack(pady=10)
tk.Button(root, text="Show My Input", command=show_input).pack()
result_label = tk.Label(root, text="")
result_label.pack(pady=10)
root.mainloop()
Password Fields: Masking Input with show="*"
Sensitive data like passwords shouldn’t be visible. Tkinter makes hiding text easy: just set show="*"
on the Entry widget. Now every character typed appears as an asterisk. The actual value is still stored in memory — you just don’t see it on screen. Security by obscurity? No. Security by UX. A small detail that makes users feel safe.
import tkinter as tk
def login():
username = user_entry.get()
password = pass_entry.get()
print(f"Login attempt: {username}, {'*' * len(password)}")
root = tk.Tk()
root.title("Secure Login")
tk.Label(root, text="Username:").pack()
user_entry = tk.Entry(root)
user_entry.pack()
tk.Label(root, text="Password:").pack()
pass_entry = tk.Entry(root, show="*") # Masks input
pass_entry.pack()
tk.Button(root, text="Login", command=login).pack(pady=10)
root.mainloop()
Checkboxes: Let Users Select Multiple Options
Checkboxes let users pick any number of items — like selecting hobbies, features, or preferences. Each checkbox is tied to a BooleanVar()
variable. When checked, it becomes True
; unchecked, False
. You read all values when the user submits, then act accordingly. Perfect for surveys, filters, or settings.
import tkinter as tk
def show_choices():
choices = []
if var1.get(): choices.append("Coffee")
if var2.get(): choices.append("Tea")
if var3.get(): choices.append("Water")
result_label.config(text=f"You chose: {', '.join(choices) if choices else 'Nothing'}")
root = tk.Tk()
root.title("Multiple Selection")
var1 = tk.BooleanVar()
var2 = tk.BooleanVar()
var3 = tk.BooleanVar()
tk.Checkbutton(root, text="Coffee", variable=var1).pack(anchor="w")
tk.Checkbutton(root, text="Tea", variable=var2).pack(anchor="w")
tk.Checkbutton(root, text="Water", variable=var3).pack(anchor="w")
tk.Button(root, text="Submit Choices", command=show_choices).pack(pady=10)
result_label = tk.Label(root, text="")
result_label.pack()
root.mainloop()
Radiobuttons: One Choice Among Many
Radiobuttons force the user to pick exactly one option — like choosing a gender, theme, or difficulty level. All radiobuttons sharing the same variable
are grouped together. Only one can be selected at a time. When you click another, the previous one automatically deselects. Simple, intuitive, and perfect for mutually exclusive choices.
import tkinter as tk
def show_selection():
selected = choice.get()
result_label.config(text=f"You selected: {selected}")
root = tk.Tk()
root.title("Single Choice")
choice = tk.StringVar(value="Option 1")
tk.Radiobutton(root, text="Option 1", variable=choice, value="Option 1").pack(anchor="w")
tk.Radiobutton(root, text="Option 2", variable=choice, value="Option 2").pack(anchor="w")
tk.Radiobutton(root, text="Option 3", variable=choice, value="Option 3").pack(anchor="w")
tk.Button(root, text="Submit", command=show_selection).pack(pady=10)
result_label = tk.Label(root, text="")
result_label.pack()
root.mainloop()
Dropdown Menus: Combobox
for Clean Selections
The classic dropdown. Uses ttk.Combobox
— part of the themed widgets module. Unlike regular OptionMenu, it looks modern and supports read-only mode. Perfect for lists of cities, products, or categories. You define the values, and the user picks one. No typing required. Clean, professional, and easy to use.
import tkinter as tk
from tkinter import ttk
def show_selected():
selected = combo.get()
result_label.config(text=f"You selected: {selected}")
root = tk.Tk()
root.title("Combobox Example")
tk.Label(root, text="Choose a fruit:").pack(pady=5)
combo = ttk.Combobox(root, values=["Apple", "Banana", "Orange", "Grape"], state="readonly")
combo.set("Select one...")
combo.pack(pady=5)
tk.Button(root, text="Confirm", command=show_selected).pack(pady=10)
result_label = tk.Label(root, text="")
result_label.pack()
root.mainloop()
Sliders: Getting Numeric Input with Drag
Sliders let users choose values within a range — volume, brightness, age, score. The Scale
widget updates continuously as the user drags. You can link it to a function using the command
parameter. Every movement triggers the function. This is ideal for real-time adjustments like audio controls or color intensity.
import tkinter as tk
def update_value(val):
label.config(text=f"Volume: {val}")
root = tk.Tk()
root.title("Slider Control")
scale = tk.Scale(root, from_=0, to=100, orient="horizontal", command=update_value)
scale.pack(pady=20)
label = tk.Label(root, text="Volume: 50")
label.pack()
root.mainloop()
Canvas: Drawing Shapes, Lines, and Text
The Canvas widget is your digital sketchpad. You can draw rectangles, ovals, lines, text, even images. It’s pixel-perfect control. Great for charts, games, diagrams, or custom visualizations. Unlike other widgets, Canvas doesn’t come with built-in interactivity — you must bind events yourself. But that’s also its strength: total creative freedom.
import tkinter as tk
root = tk.Tk()
root.title("Canvas Drawing")
canvas = tk.Canvas(root, width=300, height=200, bg="white")
canvas.pack()
canvas.create_rectangle(50, 50, 250, 150, fill="lightblue", outline="black")
canvas.create_oval(100, 80, 200, 180, fill="lightgreen")
canvas.create_line(0, 0, 300, 200, fill="red", width=3)
canvas.create_text(150, 100, text="Hello Canvas!", font=("Arial", 14))
root.mainloop()
Displaying Images: PNG, JPG, GIF Inside Tkinter
Tkinter can’t load images natively — but with PIL (Pillow), it can. You load an image file, convert it to a PhotoImage, then attach it to a Label. This opens up endless possibilities: icons, logos, photo galleries, image viewers. Remember: always keep a reference to the image object, or Python’s garbage collector will delete it and your image will vanish!
import tkinter as tk
from PIL import Image, ImageTk
# This code assumes you have a valid image file named "sample.png"
# For demonstration, let's create a dummy image if one doesn't exist.
try:
img = Image.open("sample.png")
except FileNotFoundError:
img = Image.new("RGB", (200, 150), "skyblue")
from PIL import ImageDraw
draw = ImageDraw.Draw(img)
draw.text((70, 60), "Image Here!", fill="white")
root = tk.Tk()
root.title("Display Image")
img_tk = ImageTk.PhotoImage(img)
label = tk.Label(root, image=img_tk)
label.image = img_tk # Keep a reference!
label.pack(pady=20)
root.mainloop()
Menus: Adding File, Edit, Help Bars to Your App
Every serious desktop app has a menu bar. Tkinter makes it easy. You create a Menu
widget, add cascading menus like “File” or “Help,” then assign commands. These menus look native on Windows, macOS, and Linux. Users expect them. Giving them a familiar interface makes your app feel professional — even if it’s just a simple tool.
import tkinter as tk
def open_file():
print("Open file clicked")
def save_file():
print("Save file clicked")
def about():
print("About clicked")
root = tk.Tk()
root.title("Menu Bar Example")
menu_bar = tk.Menu(root)
root.config(menu=menu_bar)
file_menu = tk.Menu(menu_bar, tearoff=0)
menu_bar.add_cascade(label="File", menu=file_menu)
file_menu.add_command(label="Open", command=open_file)
file_menu.add_command(label="Save", command=save_file)
file_menu.add_separator()
file_menu.add_command(label="Exit", command=root.quit)
help_menu = tk.Menu(menu_bar, tearoff=0)
menu_bar.add_cascade(label="Help", menu=help_menu)
help_menu.add_command(label="About", command=about)
tk.Label(root, text="Check the top menu bar!", font=("Arial", 14)).pack(pady=50)
root.mainloop()
Message Boxes: Alert, Confirm, Ask Questions
Instead of printing to console, use dialogs to communicate with users. messagebox
gives you ready-made popups: info, warning, error, yes/no questions. They’re modal — meaning the user must respond before continuing. Perfect for confirming actions (“Are you sure?”), showing errors, or giving feedback. Built-in, cross-platform, and zero setup.
import tkinter as tk
from tkinter import messagebox
def show_alert():
messagebox.showinfo("Info", "This is an info message!")
def ask_question():
result = messagebox.askyesno("Question", "Do you want to continue?")
if result:
print("User clicked Yes")
else:
print("User clicked No")
root = tk.Tk()
root.title("Message Boxes")
tk.Button(root, text="Show Alert", command=show_alert).pack(pady=10)
tk.Button(root, text="Ask Question", command=ask_question).pack(pady=10)
root.mainloop()
File Dialogs: Open Files and Folders with One Click
Users don’t want to type file paths. They want to browse. Tkinter’s filedialog
gives you system-native file browsers. You can ask for single files, multiple files, folders, or even save locations. It respects the user’s OS defaults — Mac Finder, Windows Explorer, Linux Nautilus. Makes your app feel integrated, not alien.
import tkinter as tk
from tkinter import filedialog
def open_file_dialog():
file_path = filedialog.askopenfilename(title="Select a file", filetypes=[("Text files", "*.txt"), ("All files", "*.*")])
if file_path:
label.config(text=f"Selected: {file_path}")
def open_folder_dialog():
folder_path = filedialog.askdirectory(title="Select a folder")
if folder_path:
label.config(text=f"Selected folder: {folder_path}")
root = tk.Tk()
root.title("File & Folder Dialogs")
tk.Button(root, text="Open File", command=open_file_dialog).pack(pady=10)
tk.Button(root, text="Open Folder", command=open_folder_dialog).pack(pady=10)
label = tk.Label(root, text="No file/folder selected yet.")
label.pack(pady=20)
root.mainloop()
Frames: Organize Widgets Into Logical Groups
Frames are invisible containers. They help you group related widgets together — like putting all login fields in one box, or separating buttons from text. You can give them borders, padding, colors. They make your UI look structured, not chaotic. Use frames to create sections, panels, or modular components.
import tkinter as tk
root = tk.Tk()
root.title("Frames Example")
frame1 = tk.Frame(root, borderwidth=2, relief="sunken", padx=10, pady=10)
frame1.pack(side="left", padx=20, pady=20)
tk.Label(frame1, text="Left Panel").pack()
tk.Button(frame1, text="Btn 1").pack(pady=5)
frame2 = tk.Frame(root, borderwidth=2, relief="raised", padx=10, pady=10)
frame2.pack(side="right", padx=20, pady=20)
tk.Label(frame2, text="Right Panel").pack()
tk.Button(frame2, text="Btn 2").pack(pady=5)
root.mainloop()
Keyboard Events: Respond to Any Key Press
Your app doesn’t need buttons to react. It can listen to the keyboard. Bind events like <KeyPress>
to trigger actions. This is how you make shortcuts — Ctrl+S to save, Esc to cancel. Perfect for power users who hate clicking. You get the key’s character, keycode, or modifier keys (Ctrl, Shift).
import tkinter as tk
def key_pressed(event):
label.config(text=f"Key pressed: {event.char} (keycode: {event.keycode})")
root = tk.Tk()
root.title("Keyboard Events")
root.geometry("400x200")
label = tk.Label(root, text="Press any key...", font=("Arial", 16))
label.pack(expand=True)
root.bind("<KeyPress>", key_pressed)
root.mainloop()
Mouse Events: Track Clicks, Movement, and Double Clicks
Beyond buttons, you can track the mouse anywhere. <Button-1>
is a left-click. <Motion>
tracks movement. <Double-Button-1>
catches double-clicks. Combine these with canvas or labels to build drag-and-drop, drawing apps, or interactive maps. Mouse events turn passive widgets into active tools.
import tkinter as tk
def mouse_click(event):
label.config(text=f"Clicked at ({event.x}, {event.y})")
def mouse_move(event):
status_label.config(text=f"Mouse at ({event.x}, {event.y})")
root = tk.Tk()
root.title("Mouse Events")
root.geometry("400x300")
canvas = tk.Canvas(root, bg="lightgray", width=300, height=200)
canvas.pack(pady=20)
label = tk.Label(root, text="Click anywhere on the canvas")
label.pack()
status_label = tk.Label(root, text="", fg="gray")
status_label.pack()
canvas.bind("<Button-1>", mouse_click)
canvas.bind("<Motion>", mouse_move)
root.mainloop()
Timers: Update Widgets Every Second (Like a Clock)
Want something to change automatically? Use after()
. It schedules a function to run after N milliseconds. Call it recursively, and you get a timer. Perfect for clocks, progress bars, animations, or auto-refreshing data. Never use time.sleep()
in GUI apps — it freezes everything. after()
keeps the interface responsive.
import tkinter as tk
import time
def update_clock():
current_time = time.strftime("%H:%M:%S")
clock_label.config(text=current_time)
root.after(1000, update_clock) # Schedule next update in 1000ms
root = tk.Tk()
root.title("Digital Clock")
clock_label = tk.Label(root, font=("Arial", 40), bg="black", fg="cyan")
clock_label.pack(pady=30)
update_clock() # Start the clock
root.mainloop()
Progress Bar: Show Loading Status with ttk
Users hate waiting. Give them feedback. The ttk.Progressbar
shows how far along a task is. Set it to determinate mode for known durations (like downloading a file), or indeterminate for unknown waits. Update its value programmatically to reflect progress. A small detail that makes your app feel polished and trustworthy.
import tkinter as tk
from tkinter import ttk
import time
def start_progress():
progress['value'] = 0
root.update_idletasks()
for i in range(101):
time.sleep(0.05)
progress['value'] = i
root.update_idletasks()
tk.messagebox.showinfo("Done!", "Process completed!")
root = tk.Tk()
root.title("Progress Bar")
progress = ttk.Progressbar(root, length=300, mode='determinate')
progress.pack(pady=20)
tk.Button(root, text="Start Download", command=start_progress).pack(pady=10)
root.mainloop()
Scrollable Text Box: Handle Long Content with Scrollbars
Text widgets can hold hundreds of lines — but only show a few. Add a scrollbar to let users navigate. Link the scrollbar’s yscrollcommand
to the text widget’s view, and vice versa. This is essential for logs, chat apps, code editors, or any app that displays large amounts of text.
import tkinter as tk
root = tk.Tk()
root.title("Scrollable Text Box")
text_area = tk.Text(root, wrap="word", height=10, width=50)
scrollbar = tk.Scrollbar(root, orient="vertical", command=text_area.yview)
text_area.configure(yscrollcommand=scrollbar.set)
text_area.pack(side="left", fill="both", expand=True)
scrollbar.pack(side="right", fill="y")
text_area.insert("1.0", "Type anything here...\nLine 2\nLine 3\n" * 10)
root.mainloop()
Color Chooser: Let Users Pick Any Color
Want to let users customize backgrounds, fonts, or themes? Use colorchooser.askcolor()
. It opens a native OS color picker dialog. Returns a tuple: RGB values and hex code. Store it, apply it to widgets, save it to config. Turns your app from gray and boring into personal and vibrant.
import tkinter as tk
from tkinter import colorchooser
def pick_color():
color_code = colorchooser.askcolor(title="Pick a color")
if color_code[1]: # If user picked a color (not canceled)
frame.config(bg=color_code[1])
label.config(text=f"Selected color: {color_code[1]}")
root = tk.Tk()
root.title("Color Picker")
label = tk.Label(root, text="Click below to pick a color", font=("Arial", 12))
label.pack(pady=10)
frame = tk.Frame(root, width=200, height=100, bg="white", relief="sunken", borderwidth=1)
frame.pack(pady=10)
tk.Button(root, text="Choose Color", command=pick_color).pack(pady=10)
root.mainloop()
Tabbed Interface: Organize Content with Notebook
Too many options on one screen? Split them into tabs. ttk.Notebook
lets you create tabbed panes — like browser tabs. Each tab is a separate frame. Users switch between them effortlessly. Perfect for settings panels, dashboards, or multi-step wizards.
import tkinter as tk
from tkinter import ttk
root = tk.Tk()
root.title("Tabbed Interface")
notebook = ttk.Notebook(root)
tab1 = tk.Frame(notebook)
tab2 = tk.Frame(notebook)
tab3 = tk.Frame(notebook)
notebook.add(tab1, text="Home")
notebook.add(tab2, text="Settings")
notebook.add(tab3, text="About")
tk.Label(tab1, text="Welcome to Home Tab!", font=("Arial", 16)).pack(pady=50, padx=50)
tk.Label(tab2, text="Adjust settings here.", font=("Arial", 14)).pack(pady=50, padx=50)
tk.Label(tab3, text="Made with ❤️ by Tkinter", font=("Arial", 14)).pack(pady=50, padx=50)
notebook.pack(expand=True, fill="both", padx=10, pady=10)
root.mainloop()
Custom Widget: Build Your Own Rounded Button Class
Tkinter widgets are extendable. You can inherit from Canvas
and create your own — like a rounded button. This example draws a rounded rectangle and adds click behavior. Now you can reuse it everywhere. Custom widgets make your app unique. They show you’ve moved beyond basic tutorials into true mastery.
import tkinter as tk
class RoundedButton(tk.Canvas):
def __init__(self, parent, text, command=None, width=120, height=40, radius=15, **kwargs):
# Initialize canvas with transparent background
super().__init__(parent, width=width, height=height, highlightthickness=0, **kwargs)
self.command = command
# Draw the rounded rectangle
self.create_polygon(
self._rounded_rect_points(0, 0, width, height, radius),
fill="#3498db",
smooth=True
)
# Add the text
self.create_text(width/2, height/2, text=text, fill="white", font=("Arial", 10, "bold"))
# Bind the click event
self.bind("<Button-1>", self._on_click)
def _rounded_rect_points(self, x1, y1, x2, y2, radius):
return [
x1+radius, y1, x2-radius, y1, x2, y1, x2, y1+radius,
x2, y2-radius, x2, y2, x2-radius, y2, x1+radius, y2,
x1, y2, x1, y2-radius, x1, y1+radius, x1, y1
]
def _on_click(self, event):
if self.command:
self.command()
def on_button_click():
print("Rounded button clicked!")
root = tk.Tk()
root.title("Custom Rounded Button")
btn = RoundedButton(root, "Click Me!", command=on_button_click, bg=root.cget('bg'))
btn.pack(pady=50)
root.mainloop()
Dynamic Widget Creation: Generate Buttons Based on Data
You don’t have to hardcode every button. Loop over a list and create widgets on the fly. This is how you build dynamic menus, tag clouds, product lists, or category selectors. The UI adapts to your data — not the other way around. Powerful, scalable, and elegant.
import tkinter as tk
fruits = ["Apple", "Banana", "Cherry", "Date", "Elderberry", "Fig", "Grape"]
def create_fruit_button(name):
return tk.Button(root, text=name, command=lambda: print(f"You clicked {name}"))
root = tk.Tk()
root.title("Dynamic Buttons")
for fruit in fruits:
btn = create_fruit_button(fruit)
btn.pack(pady=2, padx=20, fill='x')
root.mainloop()
Drag and Drop: Move Widgets Around the Screen
Ever used a whiteboard and moved sticky notes around? That’s drag-and-drop. With mouse events and place()
, you can make any widget movable. Bind <Button-1>
to record starting position, then <B1-Motion>
to update coordinates. It’s surprisingly simple — and incredibly satisfying to use.
import tkinter as tk
def start_drag(event):
widget = event.widget
widget._drag_start_x = event.x
widget._drag_start_y = event.y
def drag_motion(event):
widget = event.widget
x = widget.winfo_x() - widget._drag_start_x + event.x
y = widget.winfo_y() - widget._drag_start_y + event.y
widget.place(x=x, y=y)
root = tk.Tk()
root.title("Draggable Labels")
root.geometry("500x400")
label1 = tk.Label(root, text="Drag me!", bg="lightblue", relief="raised", padx=10, pady=5)
label1.place(x=50, y=50)
label1.bind("<Button-1>", start_drag)
label1.bind("<B1-Motion>", drag_motion)
label2 = tk.Label(root, text="Me too!", bg="lightcoral", relief="raised", padx=10, pady=5)
label2.place(x=200, y=150)
label2.bind("<Button-1>", start_drag)
label2.bind("<B1-Motion>", drag_motion)
root.mainloop()
Auto-resizing Window: Make Your App Responsive
Users resize windows. Your app should adapt. Use grid_rowconfigure()
and grid_columnconfigure()
with weight=1
to tell Tkinter: “Expand this row/column if space is available.” Combine with sticky="nsew"
to stretch widgets to fill their cell. Your app now feels professional on any screen size.
import tkinter as tk
root = tk.Tk()
root.title("Responsive Window")
# Configure the grid to expand
root.grid_rowconfigure(0, weight=1)
root.grid_columnconfigure(0, weight=1)
text_area = tk.Text(root)
text_area.grid(row=0, column=0, sticky="nsew")
scrollbar = tk.Scrollbar(root, orient="vertical", command=text_area.yview)
scrollbar.grid(row=0, column=1, sticky="ns")
text_area.configure(yscrollcommand=scrollbar.set)
root.mainloop()
Theme Switcher: Change Appearance at Runtime
Tired of gray buttons? Tkinter’s ttk.Style()
lets you switch themes dynamically. Try “clam,” “alt,” “default,” or “classic.” You can even create your own styles. Imagine a dark mode toggle — perfect for late-night coders. Small touches like this elevate your app from functional to delightful.
import tkinter as tk
from tkinter import ttk
def change_theme(theme_name):
style.theme_use(theme_name)
root = tk.Tk()
root.title("Theme Switcher")
style = ttk.Style(root)
# Create a frame for the buttons
button_frame = tk.Frame(root)
button_frame.pack(pady=20)
themes = style.theme_names()
for theme in themes:
btn = ttk.Button(button_frame, text=f"Theme: {theme}", command=lambda t=theme: change_theme(t))
btn.pack(pady=5, padx=20, fill='x')
ttk.Label(root, text="Sample Label").pack(pady=10)
ttk.Entry(root).pack(pady=5)
root.mainloop()
Multiple Windows: Master and Child Dialogs
Not everything belongs in one window. Use Toplevel()
to create secondary windows — like a settings popup, about dialog, or preview pane. The main window stays active. Child windows can be closed independently. This is how professional apps organize complexity without clutter.
import tkinter as tk
def open_child_window():
child = tk.Toplevel(root)
child.title("Child Window")
child.geometry("300x200")
tk.Label(child, text="This is a child window!", font=("Arial", 14)).pack(pady=50)
tk.Button(child, text="Close", command=child.destroy).pack()
root = tk.Tk()
root.title("Main Window")
root.geometry("400x300")
tk.Label(root, text="Click below to open a new window", font=("Arial", 14)).pack(pady=50)
tk.Button(root, text="Open Child", command=open_child_window).pack()
root.mainloop()
System Tray Icon: Minimize to System Tray (Advanced)
Some apps don’t need a window — just a presence. Like antivirus or music players. You can hide the window and show an icon in the system tray. Requires external libraries (pystray
+ PIL
). Once installed, you create a menu with “Show” and “Quit.” Powerful for background tools. Use wisely — it’s advanced, but rewarding.
pip install pystray pillow
Full implementation requires external code and handling OS-specific behavior. This is a conceptual overview.
Embedding Matplotlib Charts in Tkinter
Need graphs? Tkinter can host them. Use FigureCanvasTkAgg
to embed matplotlib plots inside a frame. Perfect for data analysts, scientists, or anyone visualizing trends. You get full interactivity — zoom, pan, export — while keeping your app in Python.
import tkinter as tk
from matplotlib.figure import Figure
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import numpy as np
def update_plot():
# Clear the old plot and draw a new one
ax.clear()
ax.plot(x, np.cos(x * np.random.rand()))
ax.set_title("Updated Plot")
canvas.draw()
root = tk.Tk()
root.title("Embedded Plot")
fig = Figure(figsize=(6, 4), dpi=100)
ax = fig.add_subplot(111)
x = np.linspace(0, 10, 100)
y = np.sin(x)
ax.plot(x, y)
ax.set_title("Initial Plot")
canvas = FigureCanvasTkAgg(fig, master=root)
canvas.draw()
canvas.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=1)
tk.Button(root, text="Update Plot", command=update_plot).pack(pady=10)
root.mainloop()
Save and Load Configuration with JSON
Users love settings that remember them. Save their last window size, theme, or input data to a JSON file. On startup, read it back. This transforms your tool from “one-use” to “personal assistant.” Simple, reliable, and portable across platforms.
import tkinter as tk
from tkinter import messagebox
import json
import os
CONFIG_FILE = "config.json"
def load_config():
if os.path.exists(CONFIG_FILE):
with open(CONFIG_FILE, "r") as f:
config = json.load(f)
entry_name.delete(0, tk.END)
entry_email.delete(0, tk.END)
entry_name.insert(0, config.get("name", ""))
entry_email.insert(0, config.get("email", ""))
def save_config():
config = {
"name": entry_name.get(),
"email": entry_email.get()
}
with open(CONFIG_FILE, "w") as f:
json.dump(config, f, indent=2)
messagebox.showinfo("Saved", "Configuration saved!")
root = tk.Tk()
root.title("Config Manager")
tk.Label(root, text="Name:").pack(pady=(10,0))
entry_name = tk.Entry(root, width=30)
entry_name.pack()
tk.Label(root, text="Email:").pack(pady=(10,0))
entry_email = tk.Entry(root, width=30)
entry_email.pack()
button_frame = tk.Frame(root)
button_frame.pack(pady=10)
tk.Button(button_frame, text="Load Config", command=load_config).pack(side="left", padx=5)
tk.Button(button_frame, text="Save Config", command=save_config).pack(side="left", padx=5)
# Load config on startup
load_config()
root.mainloop()
Multi-threading: Prevent GUI Freezes During Long Tasks
Never run long operations (file processing, network calls) on the main thread. It freezes your entire UI. Use threading.Thread()
to run heavy work in the background. The GUI stays responsive. Always mark threads as daemon=True
so they die when the app closes. This is non-negotiable for professional apps.
import tkinter as tk
import threading
import time
def long_task():
print("Task started in background thread...")
time.sleep(5)
print("Task finished.")
# Safely update GUI from the main thread
root.after(0, lambda: status_label.config(text="Task completed! 🎉"))
def start_task():
status_label.config(text="Working...")
# Start the long task in a new daemon thread
threading.Thread(target=long_task, daemon=True).start()
root = tk.Tk()
root.title("Non-blocking Thread")
tk.Button(root, text="Start Long Task", command=start_task).pack(pady=20)
status_label = tk.Label(root, text="Ready")
status_label.pack()
root.mainloop()
Build a Simple Calculator with Buttons
Let’s combine everything: grid layout, buttons, dynamic text, and evaluation. This calculator uses a 4x4 grid of buttons. Each button inserts text into an Entry field. The “=” button evaluates the expression with eval()
— simple but risky (don’t use in production with untrusted input!). Still, perfect for learning.
import tkinter as tk
def evaluate():
try:
result = eval(entry.get())
entry.delete(0, tk.END)
entry.insert(0, str(result))
except Exception:
entry.delete(0, tk.END)
entry.insert(0, "Error")
def clear():
entry.delete(0, tk.END)
def append_char(char):
entry.insert(tk.END, char)
root = tk.Tk()
root.title("Simple Calculator")
entry = tk.Entry(root, width=20, font=("Arial", 16), justify="right", relief="sunken", borderwidth=2)
entry.grid(row=0, column=0, columnspan=4, padx=5, pady=5)
buttons = [
('7', 1, 0), ('8', 1, 1), ('9', 1, 2), ('/', 1, 3),
('4', 2, 0), ('5', 2, 1), ('6', 2, 2), ('*', 2, 3),
('1', 3, 0), ('2', 3, 1), ('3', 3, 2), ('-', 3, 3),
('0', 4, 0), ('.', 4, 1), ('+', 4, 3),
]
for (text, row, col) in buttons:
btn = tk.Button(root, text=text, width=5, height=2, command=lambda t=text: append_char(t))
btn.grid(row=row, column=col, sticky="nsew")
# Special buttons
tk.Button(root, text='C', width=5, height=2, command=clear).grid(row=4, column=2, sticky="nsew")
tk.Button(root, text='=', width=5, height=2, command=evaluate).grid(row=5, column=0, columnspan=4, sticky="nsew")
root.mainloop()
Create a To-Do List App with Delete Functionality
Real-world apps need persistence. This simple to-do list stores tasks in a Listbox. Users type, press “Add,” and the item appears. Click “Delete” to remove the selected item. Add file saving later to make it permanent. This structure powers thousands of productivity tools.
import tkinter as tk
from tkinter import messagebox
def add_task():
task = entry.get()
if task:
listbox.insert(tk.END, task)
entry.delete(0, tk.END)
else:
messagebox.showwarning("Warning", "You must enter a task.")
def delete_task():
try:
selected_index = listbox.curselection()[0]
listbox.delete(selected_index)
except IndexError:
messagebox.showwarning("Warning", "You must select a task to delete.")
root = tk.Tk()
root.title("To-Do List")
# Main frame
main_frame = tk.Frame(root)
main_frame.pack(padx=10, pady=10)
# Entry and Add button
entry_frame = tk.Frame(main_frame)
entry_frame.pack(fill='x')
entry = tk.Entry(entry_frame, width=30)
entry.pack(side='left', fill='x', expand=True)
tk.Button(entry_frame, text="Add Task", command=add_task).pack(side='left', padx=5)
# Listbox with Scrollbar
list_frame = tk.Frame(main_frame)
list_frame.pack(pady=10)
listbox = tk.Listbox(list_frame, width=40, height=10)
listbox.pack(side='left', fill='both', expand=True)
scrollbar = tk.Scrollbar(list_frame, orient='vertical', command=listbox.yview)
scrollbar.pack(side='right', fill='y')
listbox.config(yscrollcommand=scrollbar.set)
# Delete button
tk.Button(main_frame, text="Delete Selected Task", command=delete_task).pack()
root.mainloop()
Where Do You Go From Here? Real Projects to Build
You now have dozens of working examples. You’ve seen buttons, menus, threads, databases, images, themes, even custom widgets. So what now? Build something real:
- Personal Journal App: Save entries with date/time, search by keyword, apply themes.
- Photo Organizer: Drag photos into a window, rename them, sort by date.
- Weather Dashboard: Fetch API data, display temperature, humidity, forecast.
- Pomodoro Timer: 25-minute work, 5-minute break. Sound alerts, stats tracker.
- Config Editor: Edit INI/JSON files for your Python scripts with a GUI.
- Quiz Game: Multiple-choice questions, score tracking, restart button.
Start small. Finish big. Your first app might be simple. But it will be yours.
Conclusion: Tkinter Isn’t Dead — It’s Quietly Essential
In a world obsessed with web apps, cloud services, and mobile-first design, Tkinter flies under the radar. But it’s still here. Used by universities, hospitals, government agencies, and hobbyists alike. Why? Because sometimes, the best tool isn’t the newest — it’s the one you already have. You don’t need Node.js. You don’t need React. You don’t need Docker. You just need Python. And Tkinter. Together, they turn your ideas into tangible, usable tools — not just code. So go ahead. Build something. Click a button. Watch a window appear. Feel the power of turning logic into life.