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>