Tkinter has no separate table widget. To show a table, use ttk.Treeview with show="headings": define the columns, give each a heading, and add rows with insert(). This tutorial builds that table step by step and then adds scrollbars, striped rows, sorting by column, a form to add, edit and delete rows, editing a cell in place, a search box, and loading a pandas DataFrame.
Every program is complete and was run with Python 3.12.5 (Tk 8.6.13), pandas 3.0.6, on Windows 11. The screenshots are the real windows. For the full reference, see ttk.Treeview in the Python documentation.
Create a table in Tkinter with Treeview
import tkinter as tk
from tkinter import ttk
root = tk.Tk()
root.title("Employees")
columns = ("name", "department", "city", "salary")
table = ttk.Treeview(root, columns=columns, show="headings", height=6)
# headings (the text shown at the top of each column)
table.heading("name", text="Name")
table.heading("department", text="Department")
table.heading("city", text="City")
table.heading("salary", text="Salary")
# column widths and alignment
table.column("name", width=140)
table.column("department", width=120)
table.column("city", width=110)
table.column("salary", width=90, anchor="e")
rows = [
("Emma Wilson", "Engineering", "London", "82,000"),
("Liam Brown", "Sales", "Manchester", "54,500"),
("Olivia Jones", "Marketing", "Leeds", "61,200"),
("Noah Taylor", "Engineering", "Bristol", "78,900"),
("Ava Davies", "Finance", "London", "69,300"),
]
for row in rows:
table.insert("", "end", values=row)
table.pack(padx=10, pady=10)
root.mainloop()
columns=...names the columns; these names are used inheading(),column()andset().show="headings"hides Treeview’s first “tree” column, so it looks like a plain table.height=6is the number of visible rows, not pixels.anchor="e"right-aligns numbers; use"w"(default) for text and"center"to centre.
Treeview can also show nested rows (a tree). That side of the widget is covered in Python Tkinter Treeview.
Add scrollbars to the table
Connect a vertical and a horizontal ttk.Scrollbar to the table’s yview/xview, and place all three with grid() so the scrollbars stay attached when the window is resized. stretch=False keeps the columns at their width, so the horizontal scrollbar is needed:
import tkinter as tk
from tkinter import ttk
root = tk.Tk()
root.title("Table with scrollbars")
frame = ttk.Frame(root, padding=10)
frame.pack(fill="both", expand=True)
columns = ("id", "product", "category", "price", "stock", "supplier")
table = ttk.Treeview(frame, columns=columns, show="headings", height=8)
for col in columns:
table.heading(col, text=col.title())
table.column(col, width=120, stretch=False)
for i in range(1, 51):
table.insert("", "end", values=(i, f"Product {i}", "Hardware" if i % 2 else "Software",
f"{i * 3.5:.2f}", i * 7 % 40, f"Supplier {i % 6 + 1}"))
y_scroll = ttk.Scrollbar(frame, orient="vertical", command=table.yview)
x_scroll = ttk.Scrollbar(frame, orient="horizontal", command=table.xview)
table.configure(yscrollcommand=y_scroll.set, xscrollcommand=x_scroll.set)
# grid keeps the scrollbars glued to the table's edges
table.grid(row=0, column=0, sticky="nsew")
y_scroll.grid(row=0, column=1, sticky="ns")
x_scroll.grid(row=1, column=0, sticky="ew")
frame.rowconfigure(0, weight=1)
frame.columnconfigure(0, weight=1)
root.geometry("560x280")
root.mainloop()
More scrollbar options in Python Tkinter scrollbar.
Style the table: row height, header colours and striped rows
Styling goes through ttk.Style. The clam theme lets you change heading colours (the default Windows theme ignores them). Row colours come from tags: give a row one or more tags when you insert it and set each tag’s colours with tag_configure():
import tkinter as tk
from tkinter import ttk
root = tk.Tk()
root.title("Styled table")
style = ttk.Style()
style.theme_use("clam") # a theme that allows colour changes
style.configure("Treeview", rowheight=28, font=("Segoe UI", 10))
style.configure("Treeview.Heading", font=("Segoe UI", 10, "bold"),
background="#0a1b2e", foreground="white")
style.map("Treeview", background=[("selected", "#306998")])
columns = ("name", "department", "salary")
table = ttk.Treeview(root, columns=columns, show="headings", height=6)
for col, width in zip(columns, (160, 130, 100)):
table.heading(col, text=col.title())
table.column(col, width=width, anchor="e" if col == "salary" else "w")
# striped rows: give every other row a tag with its own background
table.tag_configure("odd", background="#f1f5fa")
table.tag_configure("even", background="white")
table.tag_configure("high", foreground="#2e7d32") # salaries above 75,000
rows = [("Emma Wilson", "Engineering", 82000), ("Liam Brown", "Sales", 54500),
("Olivia Jones", "Marketing", 61200), ("Noah Taylor", "Engineering", 78900),
("Ava Davies", "Finance", 69300), ("Mia Evans", "Sales", 57800)]
for i, (name, dept, salary) in enumerate(rows):
tags = ["odd" if i % 2 else "even"]
if salary > 75000:
tags.append("high")
table.insert("", "end", values=(name, dept, f"{salary:,}"), tags=tags)
table.pack(padx=10, pady=10)
root.mainloop()
Sort the table by clicking a column heading
Give every heading a command. The sort function reads the column’s values with set(), sorts them, and reorders the rows with move(). The sort_key() helper makes numbers sort as numbers, so “9,500” comes before “82,000” (as text it would come after):
import tkinter as tk
from tkinter import ttk
def sort_key(value):
"""Numbers sort as numbers ("9,500" < "10,000"), everything else as text."""
try:
return (0, float(value.replace(",", "")))
except ValueError:
return (1, value.lower())
def sort_by(col, descending):
rows = [(table.set(item, col), item) for item in table.get_children("")]
rows.sort(key=lambda r: sort_key(r[0]), reverse=descending)
for index, (_, item) in enumerate(rows):
table.move(item, "", index)
# show an arrow on the sorted column, and flip the order on the next click
for c in columns:
table.heading(c, text=c.title())
table.heading(col, text=f"{col.title()} {'â–¼' if descending else 'â–²'}",
command=lambda: sort_by(col, not descending))
root = tk.Tk()
root.title("Click a heading to sort")
columns = ("name", "city", "salary")
table = ttk.Treeview(root, columns=columns, show="headings", height=6)
for col in columns:
table.heading(col, text=col.title(), command=lambda c=col: sort_by(c, False))
table.column(col, width=140, anchor="e" if col == "salary" else "w")
for row in [("Emma Wilson", "London", "82,000"), ("Liam Brown", "Manchester", "9,500"),
("Olivia Jones", "Leeds", "61,200"), ("Noah Taylor", "Bristol", "78,900"),
("Ava Davies", "London", "105,300"), ("Mia Evans", "York", "57,800")]:
table.insert("", "end", values=row)
table.pack(padx=10, pady=10)
root.mainloop()
Sorting from code gives the order you would expect:
# at the end of the program, instead of root.mainloop():
sort_by('salary', True)
print([table.set(i, 'salary') for i in table.get_children()])
sort_by('name', False)
print([table.set(i, 'name') for i in table.get_children()])
Output:
['105,300', '82,000', '78,900', '61,200', '57,800', '9,500']
['Ava Davies', 'Emma Wilson', 'Liam Brown', 'Mia Evans', 'Noah Taylor', 'Olivia Jones']
Add, edit and delete rows
A small form with one Entry per column turns the table into a simple data editor. Selecting a row fills the form (<<TreeviewSelect>>), Update writes the form back with item(..., values=...), and Delete removes the selected rows:
import tkinter as tk
from tkinter import ttk, messagebox
root = tk.Tk()
root.title("Add, edit and delete rows")
columns = ("name", "email", "city")
table = ttk.Treeview(root, columns=columns, show="headings", height=6)
for col, width in zip(columns, (150, 210, 110)):
table.heading(col, text=col.title())
table.column(col, width=width)
table.grid(row=0, column=0, columnspan=4, padx=10, pady=10)
for row in [("Emma Wilson", "emma@example.com", "London"),
("Liam Brown", "liam@example.com", "Manchester"),
("Olivia Jones", "olivia@example.com", "Leeds")]:
table.insert("", "end", values=row)
# a small form: one Entry per column
entries = {}
for i, col in enumerate(columns):
ttk.Label(root, text=col.title()).grid(row=1, column=i, sticky="w", padx=10)
entries[col] = ttk.Entry(root, width=22)
entries[col].grid(row=2, column=i, padx=10, sticky="w")
def form_values():
return [entries[c].get().strip() for c in columns]
def clear_form():
for entry in entries.values():
entry.delete(0, "end")
def add_row():
values = form_values()
if not all(values):
messagebox.showwarning("Missing data", "Fill in every field.")
return
table.insert("", "end", values=values)
clear_form()
def update_row():
selected = table.selection()
if selected:
table.item(selected[0], values=form_values())
def delete_row():
for item in table.selection():
table.delete(item)
clear_form()
def on_select(event=None):
selected = table.selection()
if selected:
clear_form()
for col, value in zip(columns, table.item(selected[0], "values")):
entries[col].insert(0, value)
table.bind("<<TreeviewSelect>>", on_select)
buttons = ttk.Frame(root)
buttons.grid(row=3, column=0, columnspan=4, pady=10, padx=10, sticky="w")
for text, command in [("Add", add_row), ("Update", update_row), ("Delete", delete_row), ("Clear", clear_form)]:
ttk.Button(buttons, text=text, command=command).pack(side="left", padx=(0, 8))
root.mainloop()
I also drove the form from code (add Noah, change Emma’s city to Oxford, delete Liam) to check each button. The rows left in the table:
('Emma Wilson', 'emma@example.com', 'Oxford')
('Olivia Jones', 'olivia@example.com', 'Leeds')
('Noah Taylor', 'noah@example.com', 'Bristol')
Edit a cell in place with a double-click
Treeview has no built-in cell editing, but you can place an Entry exactly over a cell. identify_row() and identify_column() find the cell under the mouse and bbox() gives its position and size. Enter or clicking elsewhere saves; Escape cancels:
import tkinter as tk
from tkinter import ttk
root = tk.Tk()
root.title("Double-click a cell to edit it")
columns = ("product", "price", "stock")
table = ttk.Treeview(root, columns=columns, show="headings", height=5)
for col in columns:
table.heading(col, text=col.title())
table.column(col, width=150)
for row in [("Keyboard", "49.99", "12"), ("Mouse", "19.99", "40"), ("Monitor", "189.00", "7")]:
table.insert("", "end", values=row)
table.pack(padx=10, pady=10)
def edit_cell(event):
item = table.identify_row(event.y)
column = table.identify_column(event.x) # "#1", "#2", ...
if not item or not column:
return
index = int(column[1:]) - 1
x, y, width, height = table.bbox(item, column)
entry = ttk.Entry(table)
entry.place(x=x, y=y, width=width, height=height)
entry.insert(0, table.item(item, "values")[index])
entry.select_range(0, "end")
entry.focus()
def save(event=None):
values = list(table.item(item, "values"))
values[index] = entry.get()
table.item(item, values=values)
entry.destroy()
entry.bind("<Return>", save)
entry.bind("<FocusOut>", save)
entry.bind("<Escape>", lambda e: entry.destroy())
table.bind("<Double-1>", edit_cell)
root.mainloop()
Typing 24.99 and pressing Enter updates the row:
('Mouse', '24.99', '40')
Search and filter the table
Keep the full data in a list and rebuild the visible rows whenever the search text changes. trace_add("write", ...) calls the function on every key press (see also Tkinter search box):
import tkinter as tk
from tkinter import ttk
ROWS = [("Emma Wilson", "Engineering", "London"), ("Liam Brown", "Sales", "Manchester"),
("Olivia Jones", "Marketing", "Leeds"), ("Noah Taylor", "Engineering", "Bristol"),
("Ava Davies", "Finance", "London"), ("Mia Evans", "Sales", "York"),
("Leo Harris", "Engineering", "London")]
root = tk.Tk()
root.title("Search the table")
search = tk.StringVar()
top = ttk.Frame(root, padding=(10, 10, 10, 0))
top.pack(fill="x")
ttk.Label(top, text="Search:").pack(side="left")
ttk.Entry(top, textvariable=search, width=30).pack(side="left", padx=6)
columns = ("name", "department", "city")
table = ttk.Treeview(root, columns=columns, show="headings", height=7)
for col in columns:
table.heading(col, text=col.title())
table.column(col, width=140)
table.pack(padx=10, pady=10)
def refresh(*args):
text = search.get().lower()
table.delete(*table.get_children())
for row in ROWS:
if text in " ".join(row).lower(): # match any column
table.insert("", "end", values=row)
search.trace_add("write", refresh) # run on every key press
refresh()
root.mainloop()
Get selected rows and export the table to CSV
selection() returns the IDs of the selected rows, item(id, "values") returns a row, and set(id, column) returns one cell. Looping over get_children() gives every row, which is all you need to save the table:
import csv
import tkinter as tk
from tkinter import ttk
root = tk.Tk()
root.withdraw() # no window needed for this demo
table = ttk.Treeview(root, columns=("name", "city"), show="headings")
for row in [("Emma Wilson", "London"), ("Liam Brown", "Manchester"), ("Ava Davies", "Leeds")]:
table.insert("", "end", values=row)
# the selected row(s)
table.selection_set(table.get_children()[1])
for item in table.selection():
print("Selected:", table.item(item, "values"))
# one cell by column name
first = table.get_children()[0]
print("City of first row:", table.set(first, "city"))
# every row -> a CSV file
with open("table.csv", "w", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
writer.writerow(table["columns"])
writer.writerows(table.item(i, "values") for i in table.get_children())
print(open("table.csv", encoding="utf-8").read())
root.destroy()
Output:
Selected: ('Liam Brown', 'Manchester')
City of first row: London
name,city
Emma Wilson,London
Liam Brown,Manchester
Ava Davies,Leeds
Show a pandas DataFrame in a Tkinter table
Use the DataFrame’s column names as the Treeview columns and insert each row from itertuples(). Numeric columns are right-aligned:
import tkinter as tk
from tkinter import ttk
import pandas as pd
df = pd.DataFrame({
"Country": ["India", "United States", "Brazil", "Japan", "Germany"],
"Capital": ["New Delhi", "Washington, D.C.", "BrasÃlia", "Tokyo", "Berlin"],
"Area (km²)": [3287263, 9833520, 8515767, 377975, 357592],
})
root = tk.Tk()
root.title("pandas DataFrame in a Tkinter table")
table = ttk.Treeview(root, columns=list(df.columns), show="headings", height=len(df))
for col in df.columns:
numeric = pd.api.types.is_numeric_dtype(df[col])
table.heading(col, text=col)
table.column(col, width=150, anchor="e" if numeric else "w")
for row in df.itertuples(index=False):
table.insert("", "end", values=[f"{v:,}" if isinstance(v, int) else v for v in row])
table.pack(padx=10, pady=10)
root.mainloop()
Alternative: an editable grid of Entry widgets
If you want every cell to be editable like a small spreadsheet, a grid of Entry widgets is simpler than Treeview. It is fine for a few dozen cells; for large data use Treeview, or a third-party widget such as tksheet:
import tkinter as tk
data = [["Item", "Qty", "Price"],
["Apples", "4", "0.50"],
["Bread", "1", "1.20"],
["Milk", "2", "0.95"]]
root = tk.Tk()
root.title("Editable grid of Entry widgets")
cells = []
for r, row in enumerate(data):
cells.append([])
for c, value in enumerate(row):
entry = tk.Entry(root, width=12, font=("Segoe UI", 11), relief="solid", bd=1,
justify="right" if c else "left")
if r == 0:
entry.config(font=("Segoe UI", 11, "bold"), bg="#e8eef5")
entry.insert(0, value)
entry.grid(row=r, column=c, sticky="nsew")
cells[r].append(entry)
def total():
amount = sum(float(cells[r][1].get()) * float(cells[r][2].get()) for r in range(1, len(cells)))
result.config(text=f"Total: {amount:.2f}")
tk.Button(root, text="Calculate total", command=total).grid(row=len(data), column=0, pady=8, sticky="w")
result = tk.Label(root, font=("Segoe UI", 11, "bold"))
result.grid(row=len(data), column=1, columnspan=2)
root.mainloop()
Treeview table vs Entry grid
ttk.Treeview | Grid of Entry | |
|---|---|---|
| Best for | Showing and selecting rows | Small editable forms |
| Rows | Thousands | A few dozen |
| Scrolling | Built in (with a Scrollbar) | Needs a Canvas |
| Sorting, selection | Yes | Write it yourself |
| Edit cells | With the double-click trick above | Always editable |
Frequently asked questions
Does Tkinter have a table widget?
No. The standard way is ttk.Treeview with show="headings", which displays rows and columns like a table. For spreadsheet-style editing you can use a grid of Entry widgets or a third-party package such as tksheet.
How do I add a scrollbar to a Tkinter table?
Create ttk.Scrollbar(orient="vertical", command=table.yview), set table.configure(yscrollcommand=scrollbar.set), and place both with grid(). Do the same with xview for a horizontal scrollbar.
How do I get the selected row from a Treeview?
Use table.selection() for the selected row IDs and table.item(row_id, "values") for the values. Bind <<TreeviewSelect>> to react when the selection changes.
How do I clear all rows from a Tkinter table?
table.delete(*table.get_children()) removes every row in one call.
Why don’t my Treeview heading colours change?
The default theme on Windows (vista) ignores heading background colours. Switch to a theme that supports them with ttk.Style().theme_use("clam").
More Tkinter guides:
Bijay Kumar is a 13-time Microsoft MVP with more than 18 years in software development, and the founder of Python Guides and TSinfo Technologies. He started out building .NET and SharePoint solutions at HP, TCS and KPIT before moving into Python, machine learning and AI, and he also builds web apps with TypeScript and React. He writes the tutorials here himself, and every example is run before publishing so you see the real output. More about Bijay · Microsoft MVP profile · LinkedIn