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>", save)
    entry.bind("<FocusOut>", save)
    entry.bind("<Escape>", lambda e: entry.destroy())

table.bind("<Double-1>", edit_cell)
root.mainloop()
Python Tkinter table editing a cell in place: an Entry box over the Price cell of the Mouse row after a double-click
Double-clicking Mouse → Price opens an editor on the cell.

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()
Python Tkinter table search box filtering the rows to employees in London
Typing “london” keeps only the matching rows.

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()
pandas DataFrame shown in a Python Tkinter Treeview table with Country, Capital and Area columns
A DataFrame displayed with Treeview.

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()
Python Tkinter table made from a grid of Entry widgets with a Calculate total button showing Total 5.10
Each cell is an Entry; the button reads them back.

Treeview table vs Entry grid

ttk.TreeviewGrid of Entry
Best forShowing and selecting rowsSmall editable forms
RowsThousandsA few dozen
ScrollingBuilt in (with a Scrollbar)Needs a Canvas
Sorting, selectionYesWrite it yourself
Edit cellsWith the double-click trick aboveAlways 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: