How to Create a Table in Python Tkinter (Treeview: Scroll, Sort, Edit, Search)

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()
Python Tkinter table created with ttk.Treeview showing Name, Department, City and Salary columns with five employee rows
A basic Tkinter table with four columns.
  • columns=... names the columns; these names are used in heading(), column() and set().
  • show="headings" hides Treeview’s first “tree” column, so it looks like a plain table.
  • height=6 is 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()
Python Tkinter table with vertical and horizontal scrollbars showing 50 product rows
50 rows and six columns in a 560 × 280 window.

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()
Python Tkinter table with striped rows, a dark header, bold heading font and green text for salaries above 75,000
Striped rows, a dark header and a highlight tag.

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()
Python Tkinter table sorted by the Salary column in descending order with a down arrow in the heading
After clicking Salary twice: highest first.

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()
Python Tkinter table with a form to add, update and delete rows, with the second row selected and its values loaded into the entry fields
Selecting Liam Brown loads the row into the form.

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>