CTkDataTable User Guide and API Reference
CTkDataTable is a virtualized data table for CustomTkinter. Use it when you want to show records from Python lists, database queries, reports, admin screens, dashboards, or audit logs without creating one Tkinter widget for every cell.
The table renders only the visible body rows, so large datasets stay responsive. Your full row list is still kept in memory.
What You Need First
CTkDataTable requires Python 3.11 or newer. Install it from PyPI with the same Python interpreter that runs your application:
python -m pip install CTkDataTableThis installs the supported dependencies automatically (customtkinter>=5.2.2,<6.1 and CTkScrollableDropdownPP>=2.2,<2.5 for this release). On Linux, Python's Tk support may also need to be installed through the operating-system package manager if import tkinter is unavailable.
Import the widget:
import customtkinter as ctk
from CTkDataTable import CTkDataTableImport only the helpers your application uses. The complete public import surface is:
from CTkDataTable import (
BadgeStyle,
CellChangeCallback,
CellChangeEvent,
CellEditRequest,
CellValidationError,
CellValidator,
ColorValue,
Column,
ColumnAlign,
ColumnDefinition,
ColumnType,
ComboOption,
ComboboxChangeCallback,
CTkDataTable,
EventOrigin,
RowSnapshot,
SelectionChangeCallback,
SelectionChangeEvent,
TableAction,
TableColumn,
TableDensity,
TableRowEvent,
TableStyle,
rows_from_cursor,
)ComboboxChangeCallback is a compatibility alias for CellChangeCallback; new annotations should normally use CellChangeCallback. See Public Imports and Typing Helpers for the exact aliases and literal values.
Project links: PyPI package, source repository, and issue tracker.
Use the table in three steps:
- Define
columns. - Provide
rows. - Place the table with
grid(),pack(), orplace().
The most important rule:
column["key"] == row_dictionary_keyFor example, this column:
{"key": "name", "title": "Name", "width": 180}reads this value from each row:
{"name": "Alice"}Use the exact same spelling and capitalization. customer_name and customerName are different keys.
Entry Point 1: Static Data Display
Use this pattern when you already have a list of records and want to show it.
import customtkinter as ctk
from CTkDataTable import CTkDataTable
app = ctk.CTk()
app.title("Customers")
app.geometry("760x420")
app.grid_columnconfigure(0, weight=1)
app.grid_rowconfigure(0, weight=1)
columns = [
{"key": "id", "title": "ID", "width": 80, "type": "number"},
{"key": "name", "title": "Customer", "width": 220},
{"key": "status", "title": "Status", "width": 140, "type": "badge"},
]
rows = [
{"id": 1, "name": "Northwind Components", "status": "Open"},
{"id": 2, "name": "Meridian Foods", "status": "Closed"},
{"id": 3, "name": "Blue Ridge Logistics", "status": "In Review"},
]
table = CTkDataTable(app, columns=columns, data=rows)
table.grid(row=0, column=0, sticky="nsew", padx=16, pady=16)
app.mainloop()Entry Point 2: Real-Time Updates
Use add_row(), add_rows(), update_row_where(), and delete_row_by_key() when rows change after the table is already visible.
import customtkinter as ctk
from CTkDataTable import CTkDataTable
app = ctk.CTk()
app.title("Live Orders")
app.geometry("820x420")
app.grid_columnconfigure(0, weight=1)
app.grid_rowconfigure(1, weight=1)
columns = [
{"key": "id", "title": "Order", "width": 100},
{"key": "customer", "title": "Customer", "width": 220},
{"key": "status", "title": "Status", "width": 140, "type": "badge"},
{"key": "amount", "title": "Amount", "width": 120, "type": "currency"},
]
rows = [
{"id": "SO-1001", "customer": "Northwind Components", "status": "Open", "amount": 1250},
{"id": "SO-1002", "customer": "Meridian Foods", "status": "Open", "amount": 890},
]
table = CTkDataTable(app, columns=columns, data=rows)
table.grid(row=1, column=0, sticky="nsew", padx=16, pady=(0, 16))
def add_order() -> None:
table.add_row(
{"id": "SO-1003", "customer": "Blue Ridge Logistics", "status": "Open", "amount": 1425}
)
def mark_shipped() -> None:
table.update_row_where(
"id",
"SO-1001",
{"id": "SO-1001", "customer": "Northwind Components", "status": "Shipped", "amount": 1250},
)
def remove_order() -> None:
table.delete_row_by_key("id", "SO-1002")
toolbar = ctk.CTkFrame(app, corner_radius=0)
toolbar.grid(row=0, column=0, sticky="ew", padx=16, pady=(16, 8))
ctk.CTkButton(toolbar, text="Add", command=add_order).grid(row=0, column=0, padx=6, pady=8)
ctk.CTkButton(toolbar, text="Ship SO-1001", command=mark_shipped).grid(row=0, column=1, padx=6, pady=8)
ctk.CTkButton(toolbar, text="Remove SO-1002", command=remove_order).grid(row=0, column=2, padx=6, pady=8)
app.mainloop()Entry Point 3: Interactive Sorting, Searching, and Filtering
Users can click sortable headers. Your code can also call sort_by(), search(), and set_column_filter().
import customtkinter as ctk
from CTkDataTable import CTkDataTable, TableRowEvent
app = ctk.CTk()
app.title("Interactive Table")
app.geometry("900x500")
app.grid_columnconfigure(0, weight=1)
app.grid_rowconfigure(2, weight=1)
status = ctk.CTkLabel(app, text="No selection", anchor="w")
status.grid(row=0, column=0, sticky="ew", padx=16, pady=(16, 4))
search = ctk.CTkEntry(app, placeholder_text="Search visible columns")
search.grid(row=1, column=0, sticky="ew", padx=16, pady=(4, 8))
columns = [
{"key": "id", "title": "ID", "width": 80, "type": "number"},
{"key": "name", "title": "Customer", "width": 220},
{"key": "status", "title": "Status", "width": 140, "type": "badge"},
{"key": "amount", "title": "Amount", "width": 120, "type": "currency"},
]
rows = [
{"id": 1, "name": "Northwind Components", "status": "Open", "amount": 1250},
{"id": 2, "name": "Meridian Foods", "status": "Closed", "amount": 890},
{"id": 3, "name": "Blue Ridge Logistics", "status": "Open", "amount": 1425},
]
def show_selected(event: TableRowEvent) -> None:
status.configure(
text=f"Selected source row {event.source_index}, visible row {event.view_index}: {event.row['name']}"
)
def report_sort(column_key: str, ascending: bool) -> None:
direction = "ascending" if ascending else "descending"
status.configure(text=f"Sorted by {column_key} {direction}")
table = CTkDataTable(
app,
columns=columns,
data=rows,
multi_select=True,
on_row_click=show_selected,
on_sort=report_sort,
)
table.grid(row=2, column=0, sticky="nsew", padx=16, pady=(0, 16))
search.bind("<KeyRelease>", lambda _event: table.search(search.get()))
table.set_column_filter("status", {"type": "equals", "value": "Open"})
table.sort_by("amount", ascending=False)
app.mainloop()Row Data
Rows are converted to dictionaries internally. The table accepts:
| Source row value | How to use it |
|---|---|
dict |
Pass directly in data, set_data(), add_row(), or add_rows(). |
| Any mapping object | Pass directly if it behaves like a dictionary. |
sqlite3.Row |
Set connection.row_factory = sqlite3.Row, then pass fetched rows directly. |
SQLAlchemy rows with _mapping |
Pass fetched result rows directly. |
| PostgreSQL dictionary rows | Use psycopg 3 dict_row or psycopg2 RealDictCursor, then pass fetched rows directly. |
| Plain DB-API tuple rows | Convert them with rows_from_cursor(cursor). |
Plain tuple rows do not contain column names, so the table cannot use them directly.
Stable Row IDs
For database-backed data, configure row_key with the field holding the primary key:
table = CTkDataTable(app, columns=columns, data=rows, row_key="customer_id")Every row must contain that field and its value must be hashable and unique. The table validates a replacement dataset before applying it, so a missing or duplicate ID does not partly replace the current rows. Stable IDs survive sorting, filtering, and source-index shifts and are exposed by events and all ..._by_id() methods. Cell editing cannot change the configured identity field.
SQLite Rows
import sqlite3
connection = sqlite3.connect("customers.db")
connection.row_factory = sqlite3.Row
rows = connection.execute(
"""
SELECT id, name, status
FROM customers
ORDER BY name
"""
).fetchall()
table.set_data(rows)DB-API Cursor Rows
from CTkDataTable import rows_from_cursor
cursor.execute(
"""
SELECT id, name, status
FROM customers
ORDER BY name
"""
)
table.set_data(rows_from_cursor(cursor))SQLAlchemy Rows
from sqlalchemy import text
with engine.connect() as connection:
result = connection.execute(
text("SELECT id, name, status FROM customers ORDER BY name")
)
table.set_data(result.fetchall())If database column names do not match your table keys, use SQL aliases:
SELECT customer_id AS id, customer_name AS name, order_status AS status
FROM customers;Column Definitions
You can define columns with dictionaries, TableColumn, or the fluent Column builder. Dictionary columns are the shortest option.
columns = [
{"key": "id", "title": "ID", "width": 80, "type": "number"},
{"key": "name", "title": "Customer", "width": 220},
]Use TableColumn when you prefer typed Python objects:
from CTkDataTable import TableColumn
columns = [
TableColumn(key="id", title="ID", width=80, type="number"),
TableColumn(key="name", title="Customer", width=220),
]Use Column when you want fluent column setup:
from CTkDataTable import Column
columns = [
Column("id").title("ID").width(80).number(),
Column("name").title("Customer").width(220).text(),
Column("status").title("Status").width(140).badge(
colors={"Open": "#22c55e", "Closed": "#64748b"},
fallback_color="#94a3b8",
),
]All Column Types in One App
This complete app demonstrates all 13 column types: text, number, percentage, currency, date, datetime, badge, checkbox, combobox, progress, link, pill_list, and action.
from datetime import date, datetime
import customtkinter as ctk
from CTkDataTable import CTkDataTable, TableRowEvent
app = ctk.CTk()
app.title("Column Types")
app.geometry("1180x520")
app.grid_columnconfigure(0, weight=1)
app.grid_rowconfigure(1, weight=1)
message = ctk.CTkLabel(app, text="Click a link or action", anchor="w")
message.grid(row=0, column=0, sticky="ew", padx=16, pady=(16, 8))
columns = [
{"key": "name", "title": "Text", "width": 180, "type": "text"},
{"key": "quantity", "title": "Number", "width": 95, "type": "number", "number_format": "{:,.0f}"},
{"key": "margin", "title": "Percentage", "width": 120, "type": "percentage", "percentage_format": "{value:.1f}%"},
{"key": "amount", "title": "Currency", "width": 120, "type": "currency", "currency_symbol": "$"},
{"key": "due_date", "title": "Date", "width": 120, "type": "date", "date_format": "%d %b %Y"},
{"key": "updated_at", "title": "Datetime", "width": 150, "type": "datetime", "datetime_format": "%d %b %H:%M"},
{
"key": "status",
"title": "Badge",
"width": 120,
"type": "badge",
"badge_colors": {"Open": "#22c55e", "Blocked": "#ef4444"},
"badge_fallback_color": "#64748b",
},
{"key": "approved", "title": "Checkbox", "width": 105, "type": "checkbox"},
{
"key": "stage",
"title": "Combobox",
"width": 145,
"type": "combobox",
"values": ["New", "Review", "Done"],
},
{"key": "progress", "title": "Progress", "width": 145, "type": "progress", "progress_text_format": "{percent:.0f}%"},
{"key": "profile", "title": "Link", "width": 120, "type": "link"},
{
"key": "tags",
"title": "Pills",
"width": 170,
"type": "pill_list",
"pill_colors": {"Urgent": "#ef4444", "Finance": "#0ea5e9"},
"pill_fallback_color": "#64748b",
"pill_text_color": "#ffffff",
},
{
"key": "actions",
"title": "Action",
"width": 170,
"type": "action",
"sortable": False,
"actions": [
{"key": "view", "label": "View"},
{"key": "delete", "label": "Delete"},
],
},
]
rows = [
{
"name": "Northwind Components",
"quantity": 1200,
"margin": 18.4,
"amount": 12640.5,
"due_date": date(2026, 6, 12),
"updated_at": datetime(2026, 6, 4, 9, 30),
"status": "Open",
"approved": True,
"stage": "Review",
"progress": 72,
"profile": "Open",
"tags": ["Urgent", "Finance"],
"actions": None,
},
{
"name": "Meridian Foods",
"quantity": 860,
"margin": 7.5,
"amount": -240.75,
"due_date": "2026-06-18",
"updated_at": "2026-06-04T15:45:00",
"status": "Blocked",
"approved": False,
"stage": "New",
"progress": 35,
"profile": "Open",
"tags": "Sales, Follow-up",
"actions": None,
},
]
def handle_link(event: TableRowEvent) -> None:
message.configure(text=f"Link clicked for {event.row['name']}")
def handle_action(event: TableRowEvent) -> None:
message.configure(text=f"{event.action_key} clicked for {event.row['name']}")
table = CTkDataTable(
app,
columns=columns,
data=rows,
horizontal_scroll=True,
on_link_click=handle_link,
on_action_click=handle_action,
)
table.grid(row=1, column=0, sticky="nsew", padx=16, pady=(0, 16))
app.mainloop()Column Type Reference
Each column type below has a complete runnable example. Copy the whole code block into a Python file and run it from an environment where customtkinter and CTkDataTable are installed.
Every column type supports these common adjustable options:
| Option | Default | Accepted values | What it changes |
|---|---|---|---|
key |
Required | String row field name. Must match row dictionary keys exactly. | Which row value the column displays. |
title |
Title-cased key for dictionary columns |
String | Header label. |
width |
140 for dictionary columns |
Integer greater than 0 |
Preferred column width in logical pixels. |
align |
Depends on type | "left", "center", or "right" |
Cell and header alignment. |
visible |
True |
True or False |
Whether the column is rendered and searched. |
sortable |
True |
True or False |
Whether header clicks sort this column. |
formatter |
None |
Callable (value, row) -> str |
Replaces built-in display formatting for that column. |
metadata |
{} |
Mapping | App-specific metadata stored with the column. |
Common Column builder methods also work with every type: .title(...), .width(...), .align(...), .hide(), .no_sort(), .fmt(...), and .metadata(...).
Text
Text is the default column type. It displays the row value as text.
import customtkinter as ctk
from CTkDataTable import CTkDataTable
app = ctk.CTk()
app.title("Text Column")
app.geometry("520x260")
app.grid_columnconfigure(0, weight=1)
app.grid_rowconfigure(0, weight=1)
columns = [
{"key": "customer", "title": "Customer", "width": 260, "type": "text"},
]
rows = [
{"customer": "Northwind Components"},
{"customer": "Meridian Foods"},
]
table = CTkDataTable(app, columns=columns, data=rows)
table.grid(row=0, column=0, sticky="nsew", padx=16, pady=16)
app.mainloop()Builder methods for text columns:
| Method | Parameters | What it sets |
|---|---|---|
Column("customer").text() |
None | Sets type="text". |
Column("customer").fmt(func) |
Callable (value, row) -> str |
Sets a custom formatter for displayed text. |
Adjustable options for text columns:
| Option | Default | Accepted values | What it changes |
|---|---|---|---|
| Common options | See common options table | See common options table | Key, title, width, alignment, visibility, sorting, formatting, and metadata. |
type |
"text" |
"text" |
Uses plain text display. |
Number
Number columns right-align by default and sort numerically.
import customtkinter as ctk
from CTkDataTable import CTkDataTable
app = ctk.CTk()
app.title("Number Column")
app.geometry("520x260")
app.grid_columnconfigure(0, weight=1)
app.grid_rowconfigure(0, weight=1)
columns = [
{
"key": "quantity",
"title": "Quantity",
"width": 140,
"type": "number",
"number_format": "{:,.0f}",
},
]
rows = [
{"quantity": 12400},
{"quantity": 820},
]
table = CTkDataTable(app, columns=columns, data=rows)
table.grid(row=0, column=0, sticky="nsew", padx=16, pady=16)
app.mainloop()Builder methods for number columns:
| Method | Parameters | What it sets |
|---|---|---|
Column("quantity").number(format=None) |
Optional format string or callable (value) -> str |
Sets type="number" and optional number_format. |
Adjustable options for number columns:
| Option | Default | Accepted values | What it changes |
|---|---|---|---|
| Common options | See common options table | See common options table | Key, title, width, alignment, visibility, sorting, formatting, and metadata. |
type |
"text" |
"number" |
Uses numeric display and numeric sorting. |
align |
"right" |
"left", "center", or "right" |
Number columns render right for "left" or "right"; use "center" to center. |
number_format |
None |
Format string using .format(number) or callable (value) -> str |
Display format for numeric values. |
Percentage
Percentage columns right-align by default, sort numerically, and format numeric row values with a percent sign.
import customtkinter as ctk
from CTkDataTable import CTkDataTable
app = ctk.CTk()
app.title("Percentage Column")
app.geometry("540x260")
app.grid_columnconfigure(0, weight=1)
app.grid_rowconfigure(0, weight=1)
columns = [
{
"key": "margin",
"title": "Margin",
"width": 150,
"type": "percentage",
"percentage_format": "{value:.1f}%",
},
]
rows = [
{"margin": 18.4},
{"margin": 7.5},
]
table = CTkDataTable(app, columns=columns, data=rows)
table.grid(row=0, column=0, sticky="nsew", padx=16, pady=16)
app.mainloop()For ratio values such as 0.184, set percentage_multiplier to 100 before display.
Builder methods for percentage columns:
| Method | Parameters | What it sets |
|---|---|---|
Column("margin").percentage(format="{value:.0f}%", multiplier=1.0) |
Keyword-only format string and display multiplier | Sets type="percentage", percentage_format, and percentage_multiplier. |
Adjustable options for percentage columns:
| Option | Default | Accepted values | What it changes |
|---|---|---|---|
| Common options | See common options table | See common options table | Key, title, width, alignment, visibility, sorting, formatting, and metadata. |
type |
"text" |
"percentage" |
Uses percentage display and numeric sorting. |
align |
"right" |
"left", "center", or "right" |
Percentage columns render right for "left" or "right"; use "center" to center. |
percentage_format |
"{value:.0f}%" |
Format string using value, raw_value, and multiplier; positional {} also receives the display value. |
Display format for percentage values. |
percentage_multiplier |
1.0 |
Number greater than 0 |
Multiplies the raw numeric value before formatting. |
Currency
Currency columns sort numerically and format money values.
import customtkinter as ctk
from CTkDataTable import CTkDataTable
app = ctk.CTk()
app.title("Currency Column")
app.geometry("560x260")
app.grid_columnconfigure(0, weight=1)
app.grid_rowconfigure(0, weight=1)
columns = [
{
"key": "amount",
"title": "Amount",
"width": 150,
"type": "currency",
"currency_symbol": "GBP ",
"currency_format": "{symbol}{value:,.2f}",
"currency_negative_format": "({symbol}{value:,.2f})",
},
]
rows = [
{"amount": 12640.5},
{"amount": -2450.75},
]
table = CTkDataTable(app, columns=columns, data=rows)
table.grid(row=0, column=0, sticky="nsew", padx=16, pady=16)
app.mainloop()Builder methods for currency columns:
| Method | Parameters | What it sets |
|---|---|---|
Column("amount").currency(symbol="$", format="{symbol}{value:,.2f}", negative_format="-{symbol}{value:,.2f}") |
Keyword-only formatting options | Sets type="currency" and currency formatting. |
Adjustable options for currency columns:
| Option | Default | Accepted values | What it changes |
|---|---|---|---|
| Common options | See common options table | See common options table | Key, title, width, alignment, visibility, sorting, formatting, and metadata. |
type |
"text" |
"currency" |
Uses currency display and numeric sorting. |
align |
"right" |
"left", "center", or "right" |
Currency columns render right for "left" or "right"; use "center" to center. |
currency_symbol |
"$" |
String | Symbol or prefix used by currency format strings. |
currency_format |
"{symbol}{value:,.2f}" |
Format string using symbol, value, and signed_value |
Format for zero and positive values. |
currency_negative_format |
"-{symbol}{value:,.2f}" |
Format string using symbol, absolute value, and signed_value |
Format for negative values. |
Date
Date columns accept datetime.date, datetime.datetime, and ISO date strings.
from datetime import date
import customtkinter as ctk
from CTkDataTable import CTkDataTable
app = ctk.CTk()
app.title("Date Column")
app.geometry("560x260")
app.grid_columnconfigure(0, weight=1)
app.grid_rowconfigure(0, weight=1)
columns = [
{"key": "due", "title": "Due", "width": 150, "type": "date", "date_format": "%d %b %Y"},
]
rows = [
{"due": date(2026, 6, 12)},
{"due": "2026-06-18"},
]
table = CTkDataTable(app, columns=columns, data=rows)
table.grid(row=0, column=0, sticky="nsew", padx=16, pady=16)
app.mainloop()Builder methods for date columns:
| Method | Parameters | What it sets |
|---|---|---|
Column("due").date(fmt="%Y-%m-%d") |
strftime format string |
Sets type="date" and date_format. |
Adjustable options for date columns:
| Option | Default | Accepted values | What it changes |
|---|---|---|---|
| Common options | See common options table | See common options table | Key, title, width, alignment, visibility, sorting, formatting, and metadata. |
type |
"text" |
"date" |
Uses date display and date-aware sorting. |
date_format |
"%Y-%m-%d" |
strftime format string |
Display format for parsed date values. |
Datetime
Datetime columns accept datetime.datetime, datetime.date, and ISO datetime strings.
from datetime import datetime
import customtkinter as ctk
from CTkDataTable import CTkDataTable
app = ctk.CTk()
app.title("Datetime Column")
app.geometry("600x260")
app.grid_columnconfigure(0, weight=1)
app.grid_rowconfigure(0, weight=1)
columns = [
{
"key": "updated",
"title": "Updated",
"width": 180,
"type": "datetime",
"datetime_format": "%d %b %H:%M",
},
]
rows = [
{"updated": datetime(2026, 6, 4, 15, 45)},
{"updated": "2026-06-05T09:15:00"},
]
table = CTkDataTable(app, columns=columns, data=rows)
table.grid(row=0, column=0, sticky="nsew", padx=16, pady=16)
app.mainloop()Builder methods for datetime columns:
| Method | Parameters | What it sets |
|---|---|---|
Column("updated").datetime(fmt="%Y-%m-%d %H:%M") |
strftime format string |
Sets type="datetime" and datetime_format. |
Adjustable options for datetime columns:
| Option | Default | Accepted values | What it changes |
|---|---|---|---|
| Common options | See common options table | See common options table | Key, title, width, alignment, visibility, sorting, formatting, and metadata. |
type |
"text" |
"datetime" |
Uses datetime display and datetime-aware sorting. |
datetime_format |
"%Y-%m-%d %H:%M" |
strftime format string |
Display format for parsed datetime values. |
Badge
Badge columns draw a rounded label for status-like values.
import customtkinter as ctk
from CTkDataTable import CTkDataTable
app = ctk.CTk()
app.title("Badge Column")
app.geometry("560x260")
app.grid_columnconfigure(0, weight=1)
app.grid_rowconfigure(0, weight=1)
columns = [
{
"key": "status",
"title": "Status",
"width": 150,
"type": "badge",
"badge_colors": {"Open": "#22c55e", "Closed": "#64748b", "Blocked": "#ef4444"},
"badge_fallback_color": "#94a3b8",
},
]
rows = [
{"status": "Open"},
{"status": "Blocked"},
]
table = CTkDataTable(app, columns=columns, data=rows)
table.grid(row=0, column=0, sticky="nsew", padx=16, pady=16)
app.mainloop()Builder methods for badge columns:
| Method | Parameters | What it sets |
|---|---|---|
Column("status").badge(colors=None, fallback_color=None, fallback_handler=None) |
Optional color mapping, fallback color, and fallback handler | Sets type="badge" and badge styling options. |
Adjustable options for badge columns:
| Option | Default | Accepted values | What it changes |
|---|---|---|---|
| Common options | See common options table | See common options table | Key, title, width, alignment, visibility, sorting, formatting, and metadata. |
type |
"text" |
"badge" |
Draws the value as a rounded badge. |
badge_colors |
{} |
Mapping of displayed text to color string or light/dark tuple | Fill color for known badge values. |
badge_fallback_color |
None |
Color string or light/dark tuple | Fill color when the value is not in badge_colors. |
badge_fallback_handler |
None |
Callable (value, row, column) -> BadgeStyle, color, or None |
Dynamic fallback text and colors. |
Checkbox
Checkbox columns display boolean row values and toggle them when clicked. New code can use the column's on_change or the table-wide on_cell_change, both of which receive CellChangeEvent. The older on_checkbox_toggle callback remains supported; it receives TableRowEvent with the updated row, column_key set to the checkbox column, and action_key set to "checkbox".
import customtkinter as ctk
from CTkDataTable import CTkDataTable, TableRowEvent
app = ctk.CTk()
app.title("Checkbox Column")
app.geometry("560x260")
app.grid_columnconfigure(0, weight=1)
app.grid_rowconfigure(0, weight=1)
columns = [
{"key": "approved", "title": "Approved", "width": 130, "type": "checkbox"},
]
rows = [
{"approved": True},
{"approved": False},
]
def handle_checkbox(event: TableRowEvent) -> None:
assert event.column_key is not None
approved = event.row[event.column_key]
table = CTkDataTable(
app,
columns=columns,
data=rows,
on_checkbox_toggle=handle_checkbox,
)
table.grid(row=0, column=0, sticky="nsew", padx=16, pady=16)
app.mainloop()Builder methods for checkbox columns:
| Method | Parameters | What it sets |
|---|---|---|
Column("approved").checkbox(editable=True, validator=None, on_change=None) |
Keyword-only editing settings | Sets type="checkbox" and its editing hooks. |
Adjustable options for checkbox columns:
| Option | Default | Accepted values | What it changes |
|---|---|---|---|
| Common options | See common options table | See common options table | Key, title, width, alignment, visibility, sorting, formatting, and metadata. |
type |
"text" |
"checkbox" |
Draws a clickable checkbox. |
align |
"center" |
"left", "center", or "right" |
Alignment. Checkbox columns default to center alignment. |
editable |
True |
True or False |
Whether a user can toggle the value. |
validator |
None |
CellEditRequest -> str or None |
Rejects a proposed toggle by returning an error message. |
on_change |
None |
CellChangeEvent -> None |
Runs after the committed row and view are updated. |
Combobox
Combobox columns keep a table-style dropdown arrow visible in every cell and open a themed, scrollable popup powered by CTkScrollableDropdownPP. Search turns on automatically when a column has more than ten displayed choices; set searchable=True or False to override that behaviour. Selecting an option updates the in-memory row; your application decides when and how to save that row to a database.
Plain strings use the same text for display and storage. ComboOption(label, value) separates a friendly label from a hashable stored value, so the table can display "In progress" while retaining "in_progress", an integer foreign key, or another database-ready value. Use either values or options in a dictionary definition, not both.
Click the arrow to choose an option. Use dropdown_height and dropdown_width to size the popup and items_per_page to paginate very long searchable lists. With allow_empty=True, Delete, Backspace, or the labelled empty option stores empty_value; customize its text with empty_label. The stored value defaults to None, which most Python database drivers map naturally to SQL NULL. With allow_custom=True, click the text area to type: Enter or clicking elsewhere commits the text, while Escape cancels it. Existing values outside the configured choices remain visible and unchanged until the user replaces them. The dropdown dependency is installed automatically with CTkDataTable.
import customtkinter as ctk
from CTkDataTable import CellChangeEvent, ComboOption, CTkDataTable
app = ctk.CTk()
app.geometry("680x300")
app.grid_columnconfigure(0, weight=1)
app.grid_rowconfigure(0, weight=1)
def status_changed(event: CellChangeEvent) -> None:
print(event.row_id, event.old_value, "->", event.new_value)
columns = [
{"key": "id", "title": "ID", "width": 80, "type": "number"},
{
"key": "status",
"title": "Status",
"width": 190,
"type": "combobox",
"options": [
ComboOption("Pending", "pending"),
ComboOption("Active", "active"),
ComboOption("Complete", "complete"),
],
"allow_custom": False,
"allow_empty": True,
"empty_value": None,
"empty_label": "No status",
"dropdown_height": 300,
"dropdown_width": 240,
"searchable": True,
"items_per_page": 25,
"on_change": status_changed,
},
]
rows = [
{"id": 101, "status": "pending"},
{"id": 102, "status": "Legacy"},
]
table = CTkDataTable(app, columns=columns, data=rows, row_key="id")
table.grid(row=0, column=0, sticky="nsew", padx=16, pady=16)
def save_to_database() -> None:
if not table.commit_edit():
print("Cannot save:", table.edit_validation_error)
return
current_rows = table.get_data()
# Run parameterized UPDATE statements using row["id"], then commit.
save_button = ctk.CTkButton(app, text="Save", command=save_to_database)
save_button.grid(row=1, column=0, pady=(0, 16))
app.mainloop()get_data() does not commit an active text editor. The explicit commit_edit() above gives Save a predictable boundary and lets validation stop the database operation. row_key="id" puts the stable database identity in event.row_id; do not use a source or view position as a database key because positions can change.
The widget does not open database connections, run SQL, or own transactions. Use parameterized statements in application code. If a save fails, roll back there and either keep the edited rows for retry or reload authoritative rows with set_data().
Builder method and adjustable options:
| Method or option | Default | What it changes |
|---|---|---|
Column("status").combobox(values, *, allow_custom=False, allow_empty=False, empty_value=None, empty_label="— None —", dropdown_height=280, dropdown_width=None, searchable=None, items_per_page=50, editable=True, validator=None, on_change=None) |
Required values |
Sets type="combobox", popup behaviour, and editing options. |
values or options |
Required | Ordered strings or ComboOption objects. Dictionary definitions accept either name; direct TableColumn uses options. |
allow_custom |
False |
Allows text values outside values. |
allow_empty |
False |
Adds a labelled empty option and permits blank custom text. |
empty_value |
None |
Value stored for a blank selection; use None for SQL NULL. |
empty_label |
"— None —" |
Text displayed for the empty choice; it must not duplicate an option label. |
dropdown_height |
280 |
Maximum popup height in logical pixels; short lists shrink to fit. Minimum 120. |
dropdown_width |
None |
Popup width in logical pixels; None uses at least the cell width or 220. |
searchable |
None |
None enables search for more than ten choices; True or False overrides it. |
items_per_page |
50 |
Maximum choices per searchable page. |
editable |
True |
Set False to display the dropdown styling without allowing user edits. |
validator |
None |
Receives CellEditRequest; return None to accept or an error string to reject. |
on_change |
None |
Receives CellChangeEvent after a successful change. |
A combobox requires at least one non-empty string or ComboOption, even when allow_empty=True. Option labels act as dropdown keys: the same label cannot represent different stored values. Repeated stored values keep the first option. empty_label must be non-blank and distinct from every option label.
Progress
Progress columns draw a numeric value as a progress bar.
Values outside progress_min/progress_max are clamped to the track visually; the stored row value is not changed.
import customtkinter as ctk
from CTkDataTable import CTkDataTable
app = ctk.CTk()
app.title("Progress Column")
app.geometry("620x260")
app.grid_columnconfigure(0, weight=1)
app.grid_rowconfigure(0, weight=1)
columns = [
{
"key": "completion",
"title": "Complete",
"width": 180,
"type": "progress",
"progress_min": 0,
"progress_max": 100,
"progress_color": "#2563eb",
"progress_background_color": "#dbeafe",
"progress_show_text": True,
"progress_text_format": "{percent:.0f}%",
},
]
rows = [
{"completion": 72},
{"completion": 35},
]
table = CTkDataTable(app, columns=columns, data=rows)
table.grid(row=0, column=0, sticky="nsew", padx=16, pady=16)
app.mainloop()Builder methods for progress columns:
| Method | Parameters | What it sets |
|---|---|---|
Column("completion").progress(minimum=0.0, maximum=100.0, color=None, background_color=None, show_text=True, text_format="{percent:.0f}%") |
Keyword-only progress settings | Sets type="progress" and progress bar options. |
Adjustable options for progress columns:
| Option | Default | Accepted values | What it changes |
|---|---|---|---|
| Common options | See common options table | See common options table | Key, title, width, alignment, visibility, sorting, formatting, and metadata. |
type |
"text" |
"progress" |
Draws a progress bar and sorts numerically. |
align |
"center" |
"left", "center", or "right" |
Alignment. Progress columns default to center alignment. |
progress_min |
0.0 |
Number less than progress_max |
Minimum value for the bar. |
progress_max |
100.0 |
Number greater than progress_min |
Maximum value for the bar. |
progress_color |
None |
Color string or light/dark tuple | Progress fill color. |
progress_background_color |
None |
Color string or light/dark tuple | Progress track color. |
progress_show_text |
True |
True or False |
Whether formatted progress text is drawn beside the bar. |
progress_text_format |
"{percent:.0f}%" |
Format string using value, minimum, maximum, min, max, percent, and ratio |
Progress text. |
Link
Link columns draw clickable text. The underline appears on hover, press, or keyboard focus, and a click calls on_link_click with a TableRowEvent when that callback is configured.
import customtkinter as ctk
from CTkDataTable import CTkDataTable, TableRowEvent
app = ctk.CTk()
app.title("Link Column")
app.geometry("620x300")
app.grid_columnconfigure(0, weight=1)
app.grid_rowconfigure(1, weight=1)
message = ctk.CTkLabel(app, text="Click a profile link", anchor="w")
message.grid(row=0, column=0, sticky="ew", padx=16, pady=(16, 8))
columns = [
{"key": "name", "title": "Customer", "width": 220},
{"key": "profile", "title": "Profile", "width": 140, "type": "link", "link_color": "#2563eb"},
]
rows = [
{"name": "Northwind Components", "profile": "Open profile"},
{"name": "Meridian Foods", "profile": "Open profile"},
]
def handle_link(event: TableRowEvent) -> None:
message.configure(text=f"Link clicked for {event.row['name']}")
table = CTkDataTable(app, columns=columns, data=rows, on_link_click=handle_link)
table.grid(row=1, column=0, sticky="nsew", padx=16, pady=(0, 16))
app.mainloop()Builder methods for link columns:
| Method | Parameters | What it sets |
|---|---|---|
Column("profile").link(color=None) |
Optional color string or light/dark tuple | Sets type="link" and optional link_color. |
Adjustable options for link columns:
| Option | Default | Accepted values | What it changes |
|---|---|---|---|
| Common options | See common options table | See common options table | Key, title, width, alignment, visibility, sorting, formatting, and metadata. |
type |
"text" |
"link" |
Draws clickable text, underlined on hover, press, or keyboard focus. |
link_color |
None |
Color string or light/dark tuple | Link text and underline color. |
Pill List
Pill list columns display tags from a list, tuple, set, frozenset, comma-separated string, or single value.
import customtkinter as ctk
from CTkDataTable import CTkDataTable
app = ctk.CTk()
app.title("Pill List Column")
app.geometry("640x260")
app.grid_columnconfigure(0, weight=1)
app.grid_rowconfigure(0, weight=1)
columns = [
{
"key": "tags",
"title": "Tags",
"width": 240,
"type": "pill_list",
"pill_colors": {"Urgent": "#ef4444", "Finance": "#0ea5e9"},
"pill_fallback_color": "#64748b",
"pill_text_color": "#ffffff",
},
]
rows = [
{"tags": ["Urgent", "Finance"]},
{"tags": "Sales, Follow-up"},
]
table = CTkDataTable(app, columns=columns, data=rows)
table.grid(row=0, column=0, sticky="nsew", padx=16, pady=16)
app.mainloop()Builder methods for pill list columns:
| Method | Parameters | What it sets |
|---|---|---|
Column("tags").pill_list(colors=None, fallback_color=None, text_color=None) |
Optional color mapping, fallback color, and text color | Sets type="pill_list" and pill styling options. |
Adjustable options for pill list columns:
| Option | Default | Accepted values | What it changes |
|---|---|---|---|
| Common options | See common options table | See common options table | Key, title, width, alignment, visibility, sorting, formatting, and metadata. |
type |
"text" |
"pill_list" |
Draws values as compact tag pills. |
pill_colors |
{} |
Mapping of pill text to color string or light/dark tuple | Fill color for known pill values. |
pill_fallback_color |
None |
Color string or light/dark tuple | Fill color for pill values not found in pill_colors. |
pill_text_color |
None |
Color string or light/dark tuple | Text color for every pill in the column. |
Action
Action columns draw row-level buttons. A click calls on_action_click with event.action_key.
import customtkinter as ctk
from CTkDataTable import CTkDataTable, TableRowEvent
app = ctk.CTk()
app.title("Action Column")
app.geometry("720x300")
app.grid_columnconfigure(0, weight=1)
app.grid_rowconfigure(1, weight=1)
message = ctk.CTkLabel(app, text="Click an action", anchor="w")
message.grid(row=0, column=0, sticky="ew", padx=16, pady=(16, 8))
columns = [
{"key": "id", "title": "ID", "width": 80, "type": "number"},
{"key": "name", "title": "Customer", "width": 220},
{
"key": "actions",
"title": "Actions",
"width": 180,
"type": "action",
"sortable": False,
"actions": [
{"key": "view", "label": "View"},
{"key": "delete", "label": "Delete", "fg_color": "#fee2e2", "text_color": "#991b1b"},
],
},
]
rows = [
{"id": 1, "name": "Northwind Components", "actions": None},
{"id": 2, "name": "Meridian Foods", "actions": None},
]
def handle_action(event: TableRowEvent) -> None:
message.configure(text=f"{event.action_key} clicked for {event.row['name']}")
table = CTkDataTable(app, columns=columns, data=rows, on_action_click=handle_action)
table.grid(row=1, column=0, sticky="nsew", padx=16, pady=(0, 16))
app.mainloop()Builder methods for action columns:
| Method | Parameters | What it sets |
|---|---|---|
Column("actions").action(buttons, sortable=False) |
Sequence of action strings, dictionaries, or TableAction objects |
Sets type="action", actions, and sortable. |
Adjustable options for action columns:
| Option | Default | Accepted values | What it changes |
|---|---|---|---|
| Common options | See common options table | See common options table | Key, title, width, alignment, visibility, sorting, formatting, and metadata. |
type |
"text" |
"action" |
Draws row-level action buttons. |
align |
"center" |
"left", "center", or "right" |
Alignment. Action columns default to center alignment. |
sortable |
True for dictionary columns; False from Column(...).action() unless changed |
True or False |
Whether header clicks sort this action column. Usually set to False. |
actions |
() |
Sequence of TableAction, mapping, or string actions |
Buttons drawn in each row. |
Major Features
Built-In Search Entry
Set searchable=True to add a search entry above the table. Use search_delay_ms to debounce typing.
table = CTkDataTable(
app,
columns=columns,
data=rows,
searchable=True,
search_delay_ms=150,
on_search=lambda query: None,
)
table.grid(row=0, column=0, sticky="nsew")Searching is case-insensitive. It checks visible, non-action columns and combines with every active column filter. Comboboxes are searched by their displayed labels; other columns use their stored values. A display-only formatter does not change search matching.
External Search Entry
Use an external CTkEntry when you want the search box in your own toolbar.
search = ctk.CTkEntry(app, placeholder_text="Search customers")
search.grid(row=0, column=0, sticky="ew")
table = CTkDataTable(app, columns=columns, data=rows)
table.grid(row=1, column=0, sticky="nsew")
search.bind("<KeyRelease>", lambda _event: table.search(search.get()))Sorting
Header clicks sort sortable columns. Clicking the same header again toggles direction. Use sort_by() from code when your app decides the sort order.
def report_sort(column_key: str, ascending: bool) -> None:
sort_state = (column_key, ascending)
table = CTkDataTable(app, columns=columns, data=rows, on_sort=report_sort)
table.sort_by("amount", ascending=False)Missing values sort last. number, percentage, currency, and progress columns sort numerically. date and datetime columns sort by parsed date values. checkbox columns sort by boolean value. Other columns sort case-insensitively as text.
Column Filters
Column filters combine with global search. A filtered column shows a header indicator.
table.set_column_filter("status", {"type": "equals", "value": "Open"})
table.set_column_filter("amount", {"type": "range", "min": 100, "max": 500})
table.set_column_filter("due", {"type": "date_range", "min": "2026-06-01", "max": "2026-06-30"})
active_filters = table.get_column_filters()
table.clear_column_filter("status")
table.clear_column_filters()Supported mapping filter definitions:
| Type | Definition | Match behavior |
|---|---|---|
contains |
{"type": "contains", "value": "north"} |
Case-insensitive substring match. |
equals |
{"type": "equals", "value": "Open"} |
Exact Python equality. |
not_equals |
{"type": "not_equals", "value": "Closed"} |
Exact Python inequality. |
in |
{"type": "in", "values": ["Open", "Paused"]} |
Value is in a non-string iterable. |
bool |
{"type": "bool", "value": True} |
bool(value) matches the expected boolean. |
range |
{"type": "range", "min": 100, "max": 500} |
Numeric value is within the optional inclusive min and max bounds. Commas in numeric strings are ignored. |
date_range |
{"type": "date_range", "min": "2026-06-01", "max": "2026-06-30"} |
Parsed date or datetime is within the optional inclusive bounds. |
You can also pass a callable:
table.set_column_filter(
"amount",
lambda value, row: value is not None and float(value) >= 1000 and row["status"] == "Open",
)Filter callables receive the stored cell value and the current internal row mapping. Treat that row as read-only. An unsupported filter type or invalid range bound raises ValueError; a non-mapping, non-callable definition raises TypeError. Applying search or filters removes selections for rows that are no longer visible and can therefore emit on_selection_change.
Selection and Multi-Select
Single selection is enabled by default. A normal click replaces the selection. With multi_select=True, Ctrl-click toggles a row on Windows/X11, Command-click toggles on macOS/Aqua, and Shift-click selects a range on every platform. Set on_selection_change when application state must follow the completed selection rather than individual mouse clicks.
from CTkDataTable import SelectionChangeEvent
def selection_changed(event: SelectionChangeEvent) -> None:
print("Selected IDs:", event.row_ids)
print("Added:", event.added_row_ids, "Removed:", event.removed_row_ids)
table = CTkDataTable(
app,
columns=columns,
data=rows,
row_key="id",
multi_select=True,
on_selection_change=selection_changed,
)
selected_row = table.get_selected_row()
selected_rows = table.get_selected_rows()
source_indices = table.get_selected_indices()
view_indices = table.get_selected_view_indices()The event contains read-only row snapshots plus current row_ids, source_indices, and view_indices. It also reports the added and removed source indices and row IDs. Stable IDs are the safest values to carry into application or database logic; view indices describe only the current filtered and sorted display.
The same operations are available from code. add, toggle, and range_select are mutually exclusive and require multi_select=True; notify=False suppresses only the selection callback.
table.select_row(0) # replace selection by source index
table.select_row_by_id(42, add=True) # add an unselected row
table.select_row_by_id(42, toggle=True) # add or remove it
table.select_row_by_id(51, range_select=True)
table.clear_selection(notify=False)Keyboard, Focus, Tooltips, and Accessibility
The table canvas participates in Tab focus and shows a focus ring for the focused row. Click the table or Tab to it before using these row-navigation keys:
| Key | Behavior |
|---|---|
| Up, Down | Move one row. |
| Page Up, Page Down | Move by one page. |
| Home, End | Move to first or last visible row. |
| Enter | Calls on_row_double_click for the focused row. |
| Shift with movement | Extends the selection range in multi-select mode. |
For a focused combobox cell that permits empty values, Delete or Backspace stores its configured empty_value. A custom-text combobox editor uses Enter to commit and Escape to cancel. Pointer users receive hover/pressed feedback and pointer cursors on sortable headers, links, actions, checkboxes, and combobox controls.
Tooltips appear only when display content is actually truncated. Checkbox and action cells do not produce text tooltips; pill-list tooltips show the complete comma-separated list. Tooltips are supplemental and should not contain information absent from the underlying row value.
Because rows and controls are drawn on one Tk Canvas rather than created as native widgets, operating-system accessibility tools may expose the table as a canvas instead of a semantic grid with one accessible object per cell. Applications that require full screen-reader grid semantics should provide an alternate accessible view or use a native widget designed for that requirement.
Bulk Actions with Multi-Select
def delete_selected() -> None:
removed = table.delete_selected_rows()
deleted_count = removed
button = ctk.CTkButton(app, text="Delete selected", command=delete_selected)
button.grid(row=0, column=0, sticky="w")
table = CTkDataTable(app, columns=columns, data=rows, multi_select=True)
table.grid(row=1, column=0, sticky="nsew")Row, Cell, Link, Action, Checkbox, and Combobox Callbacks
Row, cell, link, action, context-menu, and the compatibility checkbox callback receive TableRowEvent. Action clicks do not also fire row or cell callbacks. Link clicks fire on_link_click when that callback is set.
Successful checkbox and combobox edits use one consistent pipeline. First the model and filtered/sorted view update. Then the column's on_change receives CellChangeEvent, followed by the table-wide on_cell_change. For checkboxes, the backward-compatible on_checkbox_toggle(TableRowEvent) runs last. Initial data and redraws never emit edit events. update_cell() and update_cell_by_id() emit them only when notify=True, with origin="api"; user edits use origin="user".
For pointer interactions, a changed selection emits on_selection_change before the target callback. A normal cell click then calls on_cell_click followed by on_row_click. Action, link, checkbox, and combobox control clicks are handled as controls and do not also call the normal cell/row callbacks. Double-clicking an ordinary row calls on_row_double_click; Enter invokes the same callback for the focused row. Context-menu items call only on_context_action.
from CTkDataTable import CellChangeEvent, TableRowEvent
def row_clicked(event: TableRowEvent) -> None:
row_identity = (event.source_index, event.view_index)
def cell_clicked(event: TableRowEvent) -> None:
selected_column = event.column_key
def action_clicked(event: TableRowEvent) -> None:
selected_action = event.action_key
def checkbox_toggled(event: TableRowEvent) -> None:
assert event.column_key is not None
checked = event.row[event.column_key]
def any_cell_changed(event: CellChangeEvent) -> None:
print(event.row_id, event.column_key, event.old_value, event.new_value, event.origin)
table = CTkDataTable(
app,
columns=columns,
data=rows,
on_row_click=row_clicked,
on_cell_click=cell_clicked,
on_action_click=action_clicked,
on_cell_change=any_cell_changed,
on_checkbox_toggle=checkbox_toggled,
)CellChangeEvent.row is a read-only top-level snapshot taken after the update. If a callback mutates the table again, use the event snapshot for the completed change rather than assuming the widget's current state is still identical.
Validation and Edit Lifecycle
Checkbox and combobox columns accept validator; the table accepts cell_validator for rules shared by every editable column. Both receive CellEditRequest and return None to accept the proposed value or a message string to reject it. Column validation runs before table-wide validation and before the data changes.
from CTkDataTable import CellEditRequest, Column
def validate_status(request: CellEditRequest) -> str | None:
if request.proposed_value == "closed" and not request.row.get("approved"):
return "Approve the record before closing it."
return None
columns = [
Column("status").combobox(
["open", "closed"],
validator=validate_status,
)
]A rejected typed combobox edit stays open and exposes its message through edit_validation_error, allowing the user to correct it. commit_edit() returns False on rejection; cancel_edit() discards the active typed edit. With no active editor, commit_edit() returns True and cancel_edit() returns False. Programmatic update_cell...() calls raise CellValidationError for invalid values. Use is_editing to check whether a custom editor is active.
read_only=True disables all user checkbox and combobox changes for a table, while editable=False disables one editable column. Programmatic cell updates are still available, so application code can refresh values. Use table.set_read_only(True/False) to change the table setting later.
Use update_cell() or update_cell_by_id() when you want cell validators to run. Whole-row methods such as update_row() replace the row directly and do not run cell validators or emit CellChangeEvent.
Context Menus
Pass context_menu to show right-click row actions. On macOS/Aqua or single-button setups, Control-click opens the context menu; Command-click remains the multi-select toggle. Opening a context menu selects its target row normally rather than treating the context-menu modifier as an additive selection modifier.
from CTkDataTable import TableRowEvent
def handle_context(event: TableRowEvent) -> None:
if event.action_key == "copy_id":
app.clipboard_clear()
app.clipboard_append(str(event.row["id"]))
table = CTkDataTable(
app,
columns=columns,
data=rows,
context_menu=[
{"key": "copy_id", "label": "Copy ID"},
{"key": "view", "label": "View"},
"archive",
],
on_context_action=handle_context,
)Resizable Columns, Fill Width, and Horizontal Scroll
Set resizable_columns=True to let users drag header dividers. Set column_width_mode="fill" when the visible columns should expand or shrink with the table width. Set horizontal_scroll=True when the total minimum column width may be wider than the table.
table = CTkDataTable(
app,
columns=columns,
data=rows,
density="comfortable",
column_width_mode="fill",
resizable_columns=True,
min_column_width=64,
horizontal_scroll=True,
)
table.set_column_width("name", 260)
name_width = table.get_column_width("name")When horizontal scrolling is enabled, Shift plus mouse wheel scrolls horizontally.
In "fixed" mode, visible columns keep their configured logical widths; enable horizontal scrolling if their total can exceed the viewport. In "fill" mode, configured widths act as proportions while the table distributes the available width, without taking a visible column below its effective minimum. set_column_width() and drag resizing clamp ordinary columns to min_column_width; action columns may use a larger minimum so their configured buttons remain usable. Hidden columns do not participate in layout.
Use density="compact", "comfortable" (the default), or "spacious" to select coordinated row, header, and footer heights. An explicit row_height, header_height, or footer_height overrides that part of the preset. Scrollbars hide automatically when their content fits; pass autohide_scrollbars=False to keep applicable scrollbars visible.
Truncated display content shows its full value in a tooltip after tooltip_delay_ms (600 milliseconds by default). Set show_tooltips=False to disable these tooltips. Sortable headers, links, action buttons, and editable controls provide automatic hover and pressed feedback, while keyboard focus is shown with a focus ring.
Table Styling
Use style for table-wide colors, spacing, and corner radii. Pass a dictionary or a TableStyle object when creating the table. Later, call configure_style() to merge changes into the current style, or set_style() to replace it.
from CTkDataTable import CTkDataTable, TableStyle
table = CTkDataTable(
app,
columns=columns,
data=rows,
style={
"surface_bg": "#ffffff",
"header_bg": "#111827",
"header_text_color": "#ffffff",
"row_alt_bg": "#f8fafc",
"hover_bg": "#e0f2fe",
"selected_bg": "#2563eb",
"selected_text_color": "#ffffff",
"divider_color": "#dbe3ef",
"border_color": "#cbd5e1",
"corner_radius": 12,
"cell_padding_x": 14,
"badge_radius": 7,
"checkbox_radius": 4,
"action_radius": 6,
},
)
table.configure_style(
link_text_color="#0f766e",
checkbox_fill_checked="#16a34a",
action_bg="#eff6ff",
)
table.set_style(
TableStyle(
surface_bg=("#ffffff", "#111827"),
text_color=("#111827", "#e5e7eb"),
header_bg=("#f1f5f9", "#020617"),
)
)Color options accept normal color strings or CustomTkinter light/dark tuples. Dimension options must be non-negative numbers. style also accepts aliases such as fg_color, text, divider, and action_text; the canonical option names are listed in the TableStyle reference.
Style Hooks
Style hooks are opt-in. Set enable_style_hooks=True, then provide row_style and/or cell_style.
Style callbacks return None or a mapping with:
| Key | Effect |
|---|---|
fg_color |
Row or cell background color. |
text_color |
Row or cell text color. |
Colors can be strings or CustomTkinter light/dark tuples.
def row_style(row):
if row["status"] == "Overdue":
return {"fg_color": "#fff7ed", "text_color": "#9a3412"}
return None
def cell_style(_row, column_key, value):
if column_key == "amount" and value < 0:
return {"text_color": "#dc2626"}
return None
table = CTkDataTable(
app,
columns=columns,
data=rows,
enable_style_hooks=True,
row_style=row_style,
cell_style=cell_style,
)BadgeStyle for Dynamic Badges
Use BadgeStyle when a badge fallback handler needs to set custom text and colors.
from typing import Any, Mapping
from CTkDataTable import BadgeStyle, TableColumn
def status_fallback(value: Any, _row: Mapping[str, Any], _column: TableColumn) -> BadgeStyle:
text = str(value or "Unknown")
return BadgeStyle(text=text, fill_color="#64748b", text_color="#ffffff")
columns = [
{
"key": "status",
"title": "Status",
"width": 140,
"type": "badge",
"badge_colors": {"Open": "#22c55e", "Closed": "#64748b"},
"badge_fallback_handler": status_fallback,
}
]Footer Summaries
Set footer=True and provide summaries. Summaries run against the current visible rows after search and column filters.
table = CTkDataTable(
app,
columns=columns,
data=rows,
footer=True,
footer_height=38,
summaries={
"id": "count",
"amount": "sum",
"status": lambda visible_rows: f"{len(visible_rows)} visible",
},
)Built-in summary names are count, sum, avg, average, min, and max. count counts visible rows; numeric summaries ignore None, empty, and non-numeric values and accept commas in numeric strings. Summary callbacks receive shallow copies of the current visible rows. Unknown strings are displayed as literal footer text, and callback failures are raised as RuntimeError with the column key.
Loading State
Use set_loading(True) before a reload that you schedule back into the Tkinter event loop.
def reload_rows() -> None:
table.set_loading(True)
app.after(50, finish_reload)
def finish_reload() -> None:
table.set_data(fetch_rows())
table.set_loading(False)Async Loading
Use load_async() to run a row loader on a background thread. The widget sets its loading state, runs your function, then safely calls set_data() on the Tkinter thread.
import time
def fetch_rows():
time.sleep(1)
return [
{"id": 1, "name": "Northwind Components", "status": "Open"},
{"id": 2, "name": "Meridian Foods", "status": "Closed"},
]
def loaded(rows):
loaded_count = len(rows)
def failed(error):
last_error = error
thread = table.load_async(
fetch_rows,
on_success=loaded,
on_error=failed,
clear_on_error=False,
)fetch_rows must be callable; invalid callbacks (including None) raise TypeError before a worker starts. The function then runs on the returned daemon threading.Thread and must not read or update Tkinter widgets. set_data(), on_success, and on_error run later on the Tkinter event-loop thread. on_success receives shallow copies of the committed rows. clear_on_error=True clears existing rows before displaying the error; the default preserves them. If another async load starts before an older one finishes, only the newest result is applied.
Error State
Use set_error() to show an error message without changing rows. Use clear_error() to return to the current data view.
table.set_error("Could not refresh customer data")
table.clear_error()Constructor Reference
table = CTkDataTable(
master,
columns,
data=None,
# All remaining CTkDataTable options are keyword-only.
density="comfortable",
row_height=None,
header_height=None,
footer_height=None,
font=None,
header_font=None,
horizontal_scroll=False,
autohide_scrollbars=True,
column_width_mode="fixed",
multi_select=False,
row_key=None,
read_only=False,
searchable=False,
search_delay_ms=0,
resizable_columns=False,
min_column_width=48,
style=None,
enable_style_hooks=False,
row_style=None,
cell_style=None,
context_menu=None,
on_context_action=None,
footer=False,
summaries=None,
empty_message="No records to display",
loading_message="Loading records...",
error_message="Could not load records",
show_tooltips=True,
tooltip_delay_ms=600,
on_row_click=None,
on_row_double_click=None,
on_cell_click=None,
on_action_click=None,
on_link_click=None,
on_checkbox_toggle=None,
on_cell_change=None,
on_selection_change=None,
cell_validator=None,
on_sort=None,
on_search=None,
**kwargs,
)CTkDataTable inherits from customtkinter.CTkFrame. Visual keyword arguments such as corner_radius, border_width, fg_color, and border_color style the rounded table viewport. If omitted, the table sets corner_radius=12 and border_width=1.
| Argument | Default | Accepted values | How to apply it |
|---|---|---|---|
master |
Required | Parent widget such as CTk, CTkFrame, or a tab/container. |
First positional argument. |
columns |
Required | Sequence of dictionaries, TableColumn, or Column objects. |
CTkDataTable(app, columns=columns). |
data |
None |
Iterable of row-like mapping objects. None becomes an empty table. |
Pass at creation or call set_data(). |
density |
"comfortable" |
"compact", "comfortable", or "spacious". Sets row/header/footer heights to 34/36/32, 42/44/38, or 50/52/44. |
density="spacious". |
row_height |
None |
None or integer pixels, at least 28. None uses the density preset. |
row_height=48. |
header_height |
None |
None or integer pixels, at least 32. None uses the density preset. |
header_height=46. |
footer_height |
None |
None or integer pixels, at least 28. None uses the density preset and matters when footer=True. |
footer_height=40. |
font |
None |
Tkinter/CustomTkinter font object or tuple accepted by Canvas text items. | font=("Segoe UI", 13). |
header_font |
None |
Font object or tuple. Defaults to font when supplied, otherwise a bold table default. |
header_font=("Segoe UI", 13, "bold"). |
horizontal_scroll |
False |
True or False. Creates horizontal scrolling support; use it when fixed/minimum widths can exceed the viewport. |
horizontal_scroll=True. |
autohide_scrollbars |
True |
True or False. Hides the vertical and optional horizontal scrollbar whenever its content fits. |
autohide_scrollbars=False. |
column_width_mode |
"fixed" |
"fixed" or "fill". Fixed uses configured widths; fill distributes the visible table width across columns. |
column_width_mode="fill". |
multi_select |
False |
True or False. Enables Ctrl-click toggle on Windows/X11, Command-click toggle on macOS/Aqua, additive API selection, and Shift/API range selection. |
multi_select=True. |
row_key |
None |
Non-empty row field name or None. When set, every row needs a unique hashable value at that key. |
row_key="customer_id". |
read_only |
False |
True or False. Disables user editing for all checkbox and combobox columns. |
read_only=True. |
searchable |
False |
True or False. |
searchable=True. |
search_delay_ms |
0 |
Integer milliseconds, 0 or greater. |
search_delay_ms=150. |
resizable_columns |
False |
True or False. |
resizable_columns=True. |
min_column_width |
48 |
Integer pixels, at least 24. |
min_column_width=64. |
style |
None |
TableStyle, mapping, or None. Controls table-wide colors, spacing, and radii. |
style={"header_bg": "#111827"}. |
enable_style_hooks |
False |
True or False. Required for row_style and cell_style. |
enable_style_hooks=True. |
row_style |
None |
Callable (row) -> mapping or None. Supports fg_color and text_color. |
row_style=my_row_style. |
cell_style |
None |
Callable (row, column_key, value) -> mapping or None. Supports fg_color and text_color. |
cell_style=my_cell_style. |
context_menu |
None |
Sequence of TableAction, mapping, or string actions. |
context_menu=[{"key": "copy", "label": "Copy"}]. |
on_context_action |
None |
Callable (event: TableRowEvent) -> None. |
on_context_action=handle_context. |
footer |
False |
True or False. |
footer=True. |
summaries |
None |
Mapping of column key to summary name or callback. | summaries={"amount": "sum"}. |
empty_message |
"No records to display" |
String. | empty_message="No customers yet". |
loading_message |
"Loading records..." |
String. | loading_message="Loading customers...". |
error_message |
"Could not load records" |
String. Used by set_error(None) and load_async() failures. |
error_message="Load failed". |
show_tooltips |
True |
True or False. Shows full display content when supported cells are visually truncated; excludes checkbox and action cells. |
show_tooltips=False. |
tooltip_delay_ms |
600 |
Integer milliseconds, 0 or greater. |
tooltip_delay_ms=350. |
on_row_click |
None |
Callable (event: TableRowEvent) -> None. |
on_row_click=select_row. |
on_row_double_click |
None |
Callable (event: TableRowEvent) -> None. Also called by Enter. |
on_row_double_click=open_row. |
on_cell_click |
None |
Callable (event: TableRowEvent) -> None. |
on_cell_click=inspect_cell. |
on_action_click |
None |
Callable (event: TableRowEvent) -> None. |
on_action_click=handle_action. |
on_link_click |
None |
Callable (event: TableRowEvent) -> None. |
on_link_click=open_link. |
on_checkbox_toggle |
None |
Callable (event: TableRowEvent) -> None. Fires after a checkbox cell toggles. |
on_checkbox_toggle=handle_checkbox. |
on_cell_change |
None |
Callable (event: CellChangeEvent) -> None. Runs after the column callback for every successful notified edit. |
on_cell_change=track_change. |
on_selection_change |
None |
Callable (event: SelectionChangeEvent) -> None. Runs after selection actually changes. |
on_selection_change=sync_selection. |
cell_validator |
None |
Callable (request: CellEditRequest) -> str or None. Shared validation after column validation. |
cell_validator=validate_edit. |
on_sort |
None |
Callable (column_key: str, ascending: bool) -> None. |
on_sort=report_sort. |
on_search |
None |
Callable (query: str) -> None. Runs after built-in or programmatic global search is applied. |
on_search=report_search. |
**kwargs |
Frame defaults plus table defaults | CustomTkinter CTkFrame options such as fg_color, border_color, corner_radius, border_width. |
fg_color=("white", "#1f2937"). |
Public Imports and Typing Helpers
The package root deliberately exports the widget, its configuration/event classes, rows_from_cursor, and the following typing helpers. Import from CTkDataTable rather than private module paths.
| Export | Runtime/type definition | Purpose |
|---|---|---|
CTkDataTable |
customtkinter.CTkFrame subclass |
The table widget. |
TableColumn |
Frozen dataclass | Fully normalized column configuration. |
Column |
Fluent Mapping[str, Any] builder |
Builds a column definition through chained methods. |
ColumnDefinition |
`TableColumn | Column |
ColumnType |
Literal "text", "number", "percentage", "currency", "date", "datetime", "badge", "checkbox", "action", "progress", "link", "pill_list", or "combobox" |
Valid column type annotation. |
ColumnAlign |
Literal "left", "center", or "right" |
Valid column alignment annotation. |
TableDensity |
Literal "compact", "comfortable", or "spacious" |
Constructor density annotation. |
ColorValue |
`str | tuple[str, str]` |
TableStyle |
Frozen dataclass | Optional table-wide shape, spacing, and color overrides. |
TableAction |
Frozen dataclass | Row action button or context-menu action definition. |
BadgeStyle |
Frozen dataclass | Resolved dynamic badge text/fill/text-color result. |
ComboOption |
Frozen, slotted dataclass | One combobox display label and stored hashable value. |
TableRowEvent |
Frozen dataclass | Row/cell/link/action/context callback payload. |
CellEditRequest |
Frozen, slotted dataclass | Proposed edit supplied to validators. |
CellChangeEvent |
Frozen, slotted dataclass | Successfully committed cell-change payload. |
SelectionChangeEvent |
Frozen, slotted dataclass | Completed selection-change payload. |
CellValidationError |
ValueError subclass |
Invalid programmatic cell edit, with message, column_key, and value. |
EventOrigin |
Literal "user" or "api" |
Origin field used by edit and selection events. |
RowSnapshot |
Mapping[str, Any] |
Read-only top-level row copy used by structured events. |
CellValidator |
`Callable[[CellEditRequest], str | None]` |
CellChangeCallback |
Callable[[CellChangeEvent], None] |
Column/table cell-change callback annotation. |
ComboboxChangeCallback |
Alias of CellChangeCallback |
Backward-compatible name; prefer CellChangeCallback in new code. |
SelectionChangeCallback |
Callable[[SelectionChangeEvent], None] |
Selection callback annotation. |
rows_from_cursor |
Function | Converts a fetched DB-API cursor result to row dictionaries. |
TableStyle Reference
Use a TableStyle object or mapping at construction. Every field defaults to None, meaning the table derives a suitable value from the active CustomTkinter theme or the renderer default. A color is a ColorValue: either one Tk-compatible color string or a (light_mode_color, dark_mode_color) tuple.
TableStyle is frozen. get_style() returns the current object, set_style() replaces the entire style, and configure_style() merges supplied fields into it. Keyword fields override the same fields in a mapping/object passed during that call. Passing None for a field through configure_style() clears that override; calling set_style() with no fields clears every TableStyle override. Active theme values are then used, with any explicit top-level fg_color, border_color, corner_radius, or border_width retained as the relevant frame fallback.
table.set_style(TableStyle(header_bg=("#f8fafc", "#111827")))
table.configure_style(header_text_color=("#111827", "#f8fafc"))
table.configure_style(header_bg=None) # restore the theme-derived header fillStyle mappings reject unknown names with ValueError. Dimension fields must be numeric and non-negative. Color strings are resolved by Tk/CustomTkinter when drawn, so use valid Tk color names or hex colors. Inside a style mapping, fg_color is an alias for canvas_bg; a top-level constructor fg_color is a CTkFrame option. Prefer canonical style names when both APIs are involved.
Style resolution is intentionally layered:
- The active CustomTkinter appearance mode and theme provide base colors.
TableStyleoverrides those table-wide values, including overlapping top-level table frame colors and shape values.- Column feature options such as
link_color,progress_color, badge/pill mappings, and per-TableActioncolors override the matching normal-state feature defaults. - Hover, pressed, selected, and keyboard-focus states apply their state colors. Row hooks affect normal rows; selection and hover take priority over row-hook colors.
- A cell hook's
fg_colorortext_coloris the most local cell override. It does not replace the internal fill of a checkbox, progress bar, badge, pill, or action button unless that feature exposes its own color option.
Badge and pill labels automatically choose black or white for the stronger contrast against valid Tk fill colors while their table-wide text field remains None. An explicit cell text color, BadgeStyle.text_color, TableColumn.pill_text_color, or table-wide badge/pill text color disables that automatic choice at the applicable level.
Shape and spacing fields:
| Option | Accepted value | None fallback and effect |
|---|---|---|
corner_radius |
Number >= 0 |
12; outer table viewport radius. |
border_width |
Number >= 0 |
1; outer table viewport border width. |
cell_padding_x |
Number >= 0 |
12; horizontal inset for header, body, and footer content. |
badge_padding_x |
Number >= 0 |
10; horizontal space around badge text. |
button_padding_x |
Number >= 0 |
12; horizontal space used when auto-sizing action labels. |
badge_radius |
Number >= 0 |
Half the badge height; badge corner radius. |
checkbox_radius |
Number >= 0 |
4; checkbox corner radius. |
progress_radius |
Number >= 0 |
Half the progress-track height; progress corner radius. |
pill_radius |
Number >= 0 |
Half the pill height; pill corner radius. |
action_radius |
Number >= 0 |
5; action-button corner radius. |
Table surface and row-state colors:
| Option | Accepted value | Theme-derived fallback and effect |
|---|---|---|
canvas_bg |
ColorValue |
Surrounding/container color behind the rounded surface. |
surface_bg |
ColorValue |
Table viewport surface. |
row_bg |
ColorValue |
Normal even-row background. |
row_alt_bg |
ColorValue |
Alternating odd-row background. |
header_bg |
ColorValue |
Normal header background. |
header_hover_bg |
ColorValue |
Hovered sortable-header background. |
header_pressed_bg |
ColorValue |
Pressed sortable-header background. |
footer_bg |
ColorValue |
Summary-footer background. |
hover_bg |
ColorValue |
Hovered, unselected row background. |
selected_bg |
ColorValue |
Selected row background. |
selected_hover_bg |
ColorValue |
Selected row background while hovered. |
Text, line, and indicator colors:
| Option | Accepted value | Theme-derived fallback and effect |
|---|---|---|
text_color |
ColorValue |
Normal body text. |
hover_text_color |
ColorValue |
Body text on a hovered row. |
selected_text_color |
ColorValue |
Body text on a selected row. |
selected_hover_text_color |
ColorValue |
Body text on a selected and hovered row. |
muted_text_color |
ColorValue |
Empty/loading messages and secondary text. |
header_text_color |
ColorValue |
Header labels. |
footer_text_color |
ColorValue |
Summary-footer text. |
divider_color |
ColorValue |
Horizontal row dividers. |
header_divider_color |
ColorValue |
Header column dividers and control outlines. |
border_color |
ColorValue |
Outer rounded table border. |
sort_indicator_color |
ColorValue |
Active sort arrow and hover sort hint. |
filter_indicator_color |
ColorValue |
Active column-filter marker. |
focus_ring_color |
ColorValue |
Keyboard-focused row/link outline. |
loading_indicator_color |
ColorValue |
Loading-state spinner/indicator. |
Typed-cell and interaction colors:
| Option | Accepted value | Theme-derived fallback and effect |
|---|---|---|
badge_bg |
ColorValue |
Badge fill when no column mapping/fallback supplies one. |
badge_text_color |
ColorValue |
Badge text; None enables per-fill black/white auto contrast. |
pill_bg |
ColorValue |
Pill fill when no column mapping/fallback supplies one, including +N overflow pills. |
pill_text_color |
ColorValue |
Pill text; None enables per-fill black/white auto contrast. |
progress_bg |
ColorValue |
Default progress-track fill. |
progress_fill |
ColorValue |
Default completed progress fill. |
progress_text_color |
ColorValue |
Formatted value drawn beside the progress track. |
link_text_color |
ColorValue |
Normal link text/underline color when the column has no link_color. |
link_hover_text_color |
ColorValue |
Hovered or pressed link text/underline color. |
checkbox_fill |
ColorValue |
Unchecked checkbox fill. |
checkbox_fill_checked |
ColorValue |
Checked checkbox fill. |
checkbox_border |
ColorValue |
Normal checkbox border. |
checkbox_check |
ColorValue |
Check-mark stroke. |
action_bg |
ColorValue |
Normal action-button fill when an action has no fg_color. |
action_hover_bg |
ColorValue |
Hovered action fill; a custom per-action fill receives its own derived hover shade. |
action_pressed_bg |
ColorValue |
Pressed action fill; a custom per-action fill receives its own derived pressed shade. |
action_border |
ColorValue |
Action-button border when an action has no border_color. |
action_text_color |
ColorValue |
Action label when an action has no text_color. |
control_hover_bg |
ColorValue |
Hover feedback behind checkbox and combobox controls. |
control_pressed_bg |
ColorValue |
Pressed feedback behind checkbox and combobox controls. |
control_focus_border_color |
ColorValue |
Keyboard-focus border for checkbox and combobox controls. |
Message and tooltip colors:
| Option | Accepted value | Theme-derived fallback and effect |
|---|---|---|
error_text_color |
ColorValue |
Error-state message. |
tooltip_bg |
ColorValue |
Truncation-tooltip background. |
tooltip_text_color |
ColorValue |
Truncation-tooltip text. |
tooltip_border_color |
ColorValue |
Truncation-tooltip border. |
Supported alias names:
| Alias | Canonical option |
|---|---|
fg_color |
canvas_bg |
text, hover_text, selected_text, selected_hover_text, muted_text, header_text, footer_text |
Matching *_color option. |
divider, header_divider, table_border |
divider_color, header_divider_color, border_color. |
sort_indicator, filter_indicator, loading_indicator |
Matching *_color option. |
badge_default_bg, badge_text, pill_text, progress_text, link_text, action_text |
Matching feature-cell color option. |
focus_ring, link_hover_text, control_focus_border, error_text, tooltip_text, tooltip_border |
Matching focus, interaction, message, or tooltip color option. |
TableColumn Reference
Dictionary, Column, and TableColumn definitions normalize to TableColumn.
| Option | Default | Accepted values | How to apply it |
|---|---|---|---|
key |
Required | Unique string row field name. Must match row dictionary keys exactly. | {"key": "name"}. |
title |
Key title-cased for dictionaries and Column; required for direct TableColumn. |
String header label. | {"title": "Customer"}. |
width |
140 for dictionaries and Column; required for direct TableColumn. |
Integer logical pixels greater than 0. |
{"width": 220}. |
align |
Depends on type. |
"left", "center", or "right". |
{"align": "right"}. |
visible |
True |
True or False. Hidden columns are not rendered or searched. |
{"visible": False}. |
sortable |
True |
True or False. Controls header-click sorting. |
{"sortable": False}. |
type |
"text" |
One of the 13 column types. | {"type": "currency"}. |
formatter |
None |
Callable (value, row) -> str. Its returned text replaces built-in type formatting. |
{"formatter": lambda value, row: str(value).upper()}. |
number_format |
None |
Format string using .format(number) or callable (value) -> str. |
{"number_format": "{:,.2f}"}. |
percentage_format |
"{value:.0f}%" |
Format string using value, raw_value, and multiplier; positional {} also receives the display value. |
{"percentage_format": "{value:.1f}%"}. |
percentage_multiplier |
1.0 |
Number greater than 0. |
{"percentage_multiplier": 100}. |
currency_symbol |
"$" |
String prefix or symbol. | {"currency_symbol": "GBP "}. |
currency_format |
"{symbol}{value:,.2f}" |
Format string using symbol, value, and signed_value. |
{"currency_format": "{symbol}{value:,.0f}"}. |
currency_negative_format |
"-{symbol}{value:,.2f}" |
Format string using absolute value, plus symbol and signed_value. |
{"currency_negative_format": "({symbol}{value:,.2f})"}. |
date_format |
"%Y-%m-%d" |
strftime format string. |
{"date_format": "%d %b %Y"}. |
datetime_format |
"%Y-%m-%d %H:%M" |
strftime format string. |
{"datetime_format": "%d %b %H:%M"}. |
badge_colors |
{} |
Mapping of displayed badge text to color string or light/dark tuple. | {"badge_colors": {"Open": "#22c55e"}}. |
badge_fallback_color |
None |
Color string or light/dark tuple. | {"badge_fallback_color": "#64748b"}. |
badge_fallback_handler |
None |
Callable (value, row, column) -> BadgeStyle, color, or None. |
{"badge_fallback_handler": status_fallback}. |
pill_colors |
{} |
Mapping of pill text to color string or light/dark tuple. | {"pill_colors": {"Urgent": "#ef4444"}}. |
pill_fallback_color |
None |
Color string or light/dark tuple. | {"pill_fallback_color": "#64748b"}. |
pill_text_color |
None |
Color string or light/dark tuple. | {"pill_text_color": "#ffffff"}. |
actions |
() |
Sequence of TableAction, mapping, or string actions. |
{"actions": [{"key": "view", "label": "View"}]}. |
progress_min |
0.0 |
Number. Must be less than progress_max. |
{"progress_min": 0}. |
progress_max |
100.0 |
Number. Must be greater than progress_min. |
{"progress_max": 100}. |
progress_color |
None |
Color string or light/dark tuple. | {"progress_color": "#2563eb"}. |
progress_background_color |
None |
Color string or light/dark tuple. | {"progress_background_color": "#dbeafe"}. |
progress_show_text |
True |
True or False. |
{"progress_show_text": False}. |
progress_text_format |
"{percent:.0f}%" |
Format string using value, minimum, maximum, min, max, percent, and ratio. |
{"progress_text_format": "{value:.0f}/{maximum:.0f}"}. |
link_color |
None |
Color string or light/dark tuple. | {"link_color": "#2563eb"}. |
options or values |
() |
Ordered strings or ComboOption(label, value) objects. Use one name only; direct TableColumn uses options. |
{"options": [ComboOption("Open", 1)]}. |
allow_custom |
False |
True or False. Used by combobox. |
{"allow_custom": True}. |
allow_empty |
False |
True or False. Used by combobox. |
{"allow_empty": True}. |
empty_value |
None |
Hashable stored value used for a blank combobox choice. | {"empty_value": None}. |
empty_label |
"— None —" |
Non-blank display label for the empty combobox choice. | {"empty_label": "No status"}. |
dropdown_height |
280 |
Integer at least 120; maximum popup height in logical pixels. |
{"dropdown_height": 320}. |
dropdown_width |
None |
None or an integer at least 120; popup width in logical pixels. |
{"dropdown_width": 260}. |
searchable |
None |
None, True, or False; automatic, forced-on, or forced-off search. |
{"searchable": True}. |
items_per_page |
50 |
Positive integer page size for searchable dropdowns. | {"items_per_page": 25}. |
editable |
True for checkbox/combobox; otherwise forced False |
True or False. |
{"editable": False}. |
validator |
None |
Callable (request: CellEditRequest) -> str or None. |
{"validator": validate_status}. |
on_change |
None |
Callable (event: CellChangeEvent) -> None. |
{"on_change": status_changed}. |
metadata |
{} |
Mapping for your own app-specific data. Not rendered. | {"metadata": {"source": "crm"}}. |
Default alignment by column type:
| Type | Default alignment |
|---|---|
number, percentage, currency |
right |
checkbox, action, progress |
center |
| All other types | left |
Those type-aware defaults apply to dictionary and Column definitions. Direct TableColumn(...) construction uses its dataclass default align="left", so set align="center" explicitly for a direct checkbox/action/progress column when desired. For numeric, percentage, and currency columns, align="left" is rendered as right alignment; use align="center" for the only non-right numeric layout. The header, body, and summary always share the resulting alignment.
TableColumn.values is a read-only convenience property returning the tuple of stored values from normalized combobox options. TableColumn.from_definition(definition) returns an existing TableColumn unchanged or normalizes one mapping; applications can usually let CTkDataTable perform that normalization automatically.
Column Builder Reference
Column("key") creates a mapping accepted anywhere a column dictionary is accepted.
| Method | Parameters | Effect |
|---|---|---|
Column(key) |
key: str |
Starts a text column with the given row key. |
.title(title) |
title: str |
Sets header text. |
.width(logical_pixels) |
logical_pixels: int |
Sets preferred column width. |
.align(align) |
"left", "center", "right" |
Sets alignment. |
.hide() |
None | Sets visible=False. |
.no_sort() |
None | Sets sortable=False. |
.fmt(func) |
Callable (value, row) -> str |
Sets formatter. |
.metadata(**kwargs) |
Keyword values | Sets metadata. |
.text() |
None | Sets type="text". |
.number(format=None) |
Format string or callable | Sets type="number" and optional number_format. |
.percentage(format="{value:.0f}%", multiplier=1.0) |
Keyword-only format string and display multiplier | Sets percentage options. |
.currency(symbol="$", format="{symbol}{value:,.2f}", negative_format="-{symbol}{value:,.2f}") |
Keyword-only formatting options | Sets currency options. |
.date(fmt="%Y-%m-%d") |
strftime format |
Sets date options. |
.datetime(fmt="%Y-%m-%d %H:%M") |
strftime format |
Sets datetime options. |
.badge(colors=None, fallback_color=None, fallback_handler=None) |
Badge settings | Sets badge options. |
.pill_list(colors=None, fallback_color=None, text_color=None) |
Pill settings | Sets pill-list options. |
.checkbox(editable=True, validator=None, on_change=None) |
Keyword-only editing settings | Sets checkbox editing options. |
.combobox(values, *, allow_custom=False, allow_empty=False, empty_value=None, empty_label="— None —", dropdown_height=280, dropdown_width=None, searchable=None, items_per_page=50, editable=True, validator=None, on_change=None) |
Strings or ComboOption objects plus popup and editing settings |
Sets combobox options and its scrollable popup. |
.progress(minimum=0.0, maximum=100.0, color=None, background_color=None, show_text=True, text_format="{percent:.0f}%") |
Progress settings | Sets progress options. |
.link(color=None) |
Optional color | Sets link options. |
.action(buttons, sortable=False) |
Sequence of actions | Sets action button options. |
TableAction Reference
TableAction defines a button inside an action column or an item in context_menu.
from CTkDataTable import TableAction
TableAction("view", "View")
TableAction("delete", "Delete", fg_color="#fee2e2", text_color="#991b1b")Dictionary and string forms are accepted:
{"key": "view", "label": "View", "width": 72}
"archive"String actions use the string as key and title-case it for the label.
Native context menus use only key and label. width and color fields apply to Canvas-rendered action-column buttons, not operating-system menu items.
TableAction.from_definition(definition) performs the same normalization for one existing TableAction, mapping, or string. It is mainly useful when application code builds reusable action registries; normal column/context-menu inputs are normalized automatically.
| Option | Default | Accepted values | How to apply it |
|---|---|---|---|
key |
Required | String action identifier. | {"key": "view"}. |
label |
key.title() for dictionaries and strings; required for direct TableAction. |
Button/menu text. | {"label": "View"}. |
width |
None |
Positive integer logical pixels. If omitted, the button is measured from the label. | {"width": 76}. |
fg_color |
None |
Color string or light/dark tuple. | {"fg_color": "#fee2e2"}. |
text_color |
None |
Color string or light/dark tuple. | {"text_color": "#991b1b"}. |
border_color |
None |
Color string or light/dark tuple. | {"border_color": "#fecaca"}. |
BadgeStyle Reference
Use BadgeStyle from a badge_fallback_handler.
BadgeStyle(text="Unknown", fill_color="#64748b", text_color="#ffffff")| Field | Default | Accepted values | Effect |
|---|---|---|---|
text |
Required | String. | Text drawn inside the badge. |
fill_color |
Required | Color string or light/dark tuple. | Badge fill color. |
text_color |
None |
Color string, light/dark tuple, or None. |
Badge text color. |
Fallback handler return values:
| Return value | Result |
|---|---|
BadgeStyle(...) |
Uses custom badge text, fill color, and optional text color. |
| Color string or tuple | Keeps the original cell text and uses the returned fill color. |
None |
Uses badge_fallback_color, then the table default if no fallback color exists. |
ComboOption Reference
Use ComboOption when displayed text should differ from the stored value:
ComboOption(label="In progress", value="in_progress")
ComboOption(label="Priority customer", value=7)Both label and value participate in combobox input matching, but the row stores value. Labels must be unique strings because they are the dropdown's user-facing selection key, and values must be hashable. If stored values repeat, the first option is retained.
CellEditRequest and Validation Reference
Column and table validators receive CellEditRequest before data changes.
| Attribute | Type | Meaning |
|---|---|---|
widget |
CTkDataTable |
Table proposing the edit. |
row_id |
Hashable or None |
Stable ID when row_key is configured. |
row |
Mapping[str, Any] |
Read-only top-level snapshot before the edit. |
source_index |
int |
Current source position. |
view_index |
int or None |
Current visible position, or None when filtered out. |
column_key |
str |
Field being edited. |
old_value |
Any |
Current stored value. |
proposed_value |
Any |
Normalized value to validate. |
origin |
"user" or "api" |
Where the edit came from. |
Return None to accept or a string to reject. Invalid programmatic edits raise CellValidationError, whose public fields are message, column_key, and value.
The row mapping is a read-only top-level copy: keys cannot be assigned through the event, but nested mutable values are not deep-copied. Treat nested values as read-only as well.
CellChangeEvent Reference
Column on_change and table on_cell_change callbacks receive CellChangeEvent after the committed model and view update.
| Attribute | Type | Meaning |
|---|---|---|
widget |
CTkDataTable |
Table that completed the change. |
row_id |
Hashable or None |
Stable ID when row_key is configured. |
row |
Mapping[str, Any] |
Read-only top-level snapshot after the change. |
source_index |
int |
Source position after the change. |
view_index_before |
int or None |
Visible position before the change. |
view_index_after |
int or None |
Visible position after reapplying sort/filter rules. |
column_key |
str |
Changed field. |
old_value |
Any |
Previous stored value. |
new_value |
Any |
New stored value. |
origin |
"user" or "api" |
User interaction or a notified API update. |
As with CellEditRequest, row is a read-only top-level snapshot rather than a recursive deep copy.
SelectionChangeEvent Reference
on_selection_change receives a completed SelectionChangeEvent only when the selection actually changes.
| Attribute | Type | Meaning |
|---|---|---|
widget |
CTkDataTable |
Table whose selection changed. |
rows |
tuple[Mapping, ...] |
Current read-only row snapshots. |
added_rows |
tuple[Mapping, ...] |
Read-only snapshots newly added by this operation. |
removed_rows |
tuple[Mapping, ...] |
Read-only snapshots removed by this operation. |
row_ids |
tuple[Hashable or None, ...] |
Current stable IDs, or None entries without row_key. |
source_indices |
tuple[int, ...] |
Current source positions. |
view_indices |
tuple[int, ...] |
Current visible positions. |
added_source_indices, removed_source_indices |
tuple[int, ...] |
Source positions changed by this operation. |
added_row_ids, removed_row_ids |
tuple[Hashable or None, ...] |
Stable identities changed by this operation. |
origin |
"user" or "api" |
User interaction or a public selection method. |
All three row tuples contain read-only top-level copies. Their nested values are shared shallowly and should be treated as read-only. The tuple fields themselves are immutable snapshots; later selection changes do not rewrite an earlier event.
TableRowEvent Reference
Row/cell/link/action/context callbacks and the backward-compatible on_checkbox_toggle receive TableRowEvent.
| Attribute | Type | Meaning |
|---|---|---|
row |
dict[str, Any] |
Mutable shallow copy of the row for this callback; changing it does not update the table. |
source_index |
int |
Index in the original source data list. |
view_index |
int |
Index in the current filtered and sorted view. |
column_key |
str or None |
Clicked column key for cell, link, and action events. |
action_key |
str or None |
Clicked action key. Link events use "link" and checkbox events use "checkbox". |
Use source_index when you want the original data position. Use view_index when you intentionally want the current visible order after search, filters, and sorting.
Method Reference
Read-only properties:
| Property | Type | Meaning |
|---|---|---|
row_key |
str or None |
Configured stable identity field. |
read_only |
bool |
Whether user editing is disabled table-wide. |
is_editing |
bool |
Whether a custom combobox text editor is active. |
edit_validation_error |
str or None |
Current active-editor validation message. |
| Method | Returns | Use |
|---|---|---|
set_data(data) |
None |
Replace all rows and clear selection. |
get_data() |
list[dict] |
Pure read of shallow copies of committed source rows; does not commit an active editor. |
commit_edit() |
bool |
Commit the active typed editor; False means validation rejected it. |
cancel_edit() |
bool |
Discard the active typed editor and report whether one was active. |
set_read_only(state) |
None |
Enable or disable user editing for the whole table. |
get_columns() |
tuple[TableColumn, ...] |
Get normalized column definitions. |
set_columns(columns) |
None |
Replace columns while preserving compatible sort/filter/selection state. |
set_column_width(column_key, width) |
None |
Set one column width, clamped to its effective minimum; action-button content can raise that minimum. |
get_column_width(column_key) |
int |
Read one column width. |
set_column_width_mode(mode) |
None |
Switch between "fixed" and "fill" column layout. |
get_column_width_mode() |
"fixed" or "fill" |
Read the current column layout mode. |
refresh() |
None |
Redraw without changing rows or state. |
get_style() |
TableStyle |
Return the current table-wide style options. |
set_style(style=None, **kwargs) |
None |
Replace table-wide style options and redraw. |
configure_style(style=None, **kwargs) |
None |
Merge table-wide style options into the current style and redraw. |
clear() |
None |
Remove all rows. |
get_selected_row() |
dict or None |
Get the first selected row. |
get_selected_rows() |
list[dict] |
Get all selected rows. |
get_selected_row_ids() |
list[Hashable] |
Get selected stable IDs in view order; requires row_key. |
get_selected_indices() |
list[int] |
Get selected source indices in current view order. |
get_selected_view_indices() |
list[int] |
Get selected visible row indices. |
select_row(source_index, *, add=False, toggle=False, range_select=False, notify=True) |
bool |
Select a visible source row. Add/toggle/range modes are mutually exclusive and require multi_select=True; returns whether selection changed. |
select_row_by_id(row_id, *, add=False, toggle=False, range_select=False, notify=True) |
bool |
Select a visible row by stable ID; requires row_key. |
clear_selection(*, notify=True) |
bool |
Clear selection and report whether it changed. |
get_row(index) |
dict |
Get a source row by source index. |
get_row_by_id(row_id) |
dict |
Get a row by its configured stable ID. |
row_id_for_source_index(source_index) |
Hashable |
Get a stable ID from a source position. |
source_index_for_row_id(row_id) |
int |
Resolve a stable ID to its current source position. |
get_view_row(view_index) |
dict |
Get a row by visible view index. |
get_cell(source_index, column_key) |
Any |
Pure read of one committed cell by source position. |
get_cell_by_id(row_id, column_key) |
Any |
Pure read of one committed cell by stable ID. |
update_cell(source_index, column_key, value, *, notify=False) |
bool |
Validate and update a cell; returns whether its value changed. notify=True emits column/table edit callbacks. |
update_cell_by_id(row_id, column_key, value, *, notify=False) |
bool |
Validate and update a cell by stable ID; requires row_key. |
source_index_for_view_index(view_index) |
int |
Convert visible index to source index. |
view_index_for_source_index(source_index) |
int or None |
Convert source index to visible index, or None if hidden by filters/search. |
find_row_index(column_key, value) |
int or None |
Find the first source row where the column equals value. |
sort_by(column_key, ascending=True) |
None |
Sort the view without reordering source data. Unlike header clicks, this API can sort a column whose sortable flag is false. |
search(query) |
None |
Case-insensitive global search across visible, non-action columns. |
filter(query) |
None |
Backward-compatible alias for search(query). |
set_column_filter(column_key, definition) |
None |
Add or replace one column filter. |
clear_column_filter(column_key) |
None |
Clear one column filter. |
clear_column_filters() |
None |
Clear all column filters. |
get_column_filters() |
dict |
Get active column filters. |
add_row(row) |
int |
Append one row and return its source index. |
add_rows(rows) |
list[int] |
Append multiple rows and return their source indices. |
update_row(index, row) |
None |
Replace a source row by source index. |
update_row_by_id(row_id, row) |
None |
Replace a source row by stable ID. |
update_view_row(view_index, row) |
None |
Replace a row by current visible index. |
update_row_where(column_key, value, new_row) |
bool |
Replace the first source row matching column_key == value. |
delete_row(index) |
None |
Delete a source row by source index. |
delete_row_by_id(row_id) |
None |
Delete a row by stable ID. |
delete_view_row(view_index) |
None |
Delete a row by current visible index. |
delete_row_where(column_key, value) |
bool |
Delete the first source row matching column_key == value. |
delete_row_by_key(column_key, value) |
bool |
Alias for delete_row_where(). |
delete_selected_rows() |
int |
Delete selected source rows and return the number removed. |
set_loading(state) |
None |
Show or hide loading state. |
set_error(message=None) |
None |
Show error state. None or an empty string uses the constructor's error_message. |
clear_error() |
None |
Hide error state without changing rows. |
load_async(fetch_rows, *, on_success=None, on_error=None, clear_on_error=False) |
threading.Thread |
Fetch rows on a daemon worker; apply data and callbacks on the Tkinter thread. Only the newest outstanding load is applied. |
destroy() |
None |
Cancel pending table callbacks/popups and destroy the widget. Call this for normal CustomTkinter teardown; do not use the object afterward. |
Data Method Examples
table.set_data([
{"id": 1, "name": "Alice", "status": "Open"},
{"id": 2, "name": "Bob", "status": "Closed"},
])
if table.commit_edit():
rows = table.get_data()
table.clear()source_index = table.add_row({"id": 3, "name": "Diana", "status": "Open"})
source_indices = table.add_rows([
{"id": 4, "name": "Evan", "status": "Open"},
{"id": 5, "name": "Fatima", "status": "Closed"},
])table.update_row(0, {"id": 1, "name": "Alice Updated", "status": "Open"})
table.update_row_by_id(2, {"id": 2, "name": "Bob Updated", "status": "Closed"})
table.update_view_row(0, {"id": 2, "name": "Bob Updated", "status": "Closed"})
updated = table.update_row_where(
"id",
3,
{"id": 3, "name": "Diana Updated", "status": "Open"},
)table.delete_row(0)
table.delete_view_row(0)
deleted = table.delete_row_where("id", 5)
deleted_again = table.delete_row_by_key("id", 4)
table.delete_row_by_id(3)status = table.get_cell(0, "status")
same_status = table.get_cell_by_id(2, "status")
table.update_cell(0, "status", "closed")
table.update_cell_by_id(2, "status", "open", notify=True)Navigation and Selection Method Examples
first_source_row = table.get_row(0)
first_visible_row = table.get_view_row(0)
source_index = table.source_index_for_view_index(0)
view_index = table.view_index_for_source_index(source_index)
found_index = table.find_row_index("id", 42)
stable_id = table.row_id_for_source_index(source_index)
same_source_index = table.source_index_for_row_id(stable_id)one_row = table.get_selected_row()
many_rows = table.get_selected_rows()
source_indices = table.get_selected_indices()
view_indices = table.get_selected_view_indices()
row_ids = table.get_selected_row_ids()
table.select_row_by_id(42)
table.select_row_by_id(43, add=True)
table.select_row_by_id(43, toggle=True)
table.select_row_by_id(51, range_select=True)
table.clear_selection()Column Method Examples
columns = table.get_columns()
table.set_columns([
{"key": "id", "title": "ID", "width": 80, "type": "number"},
{"key": "name", "title": "Customer", "width": 240},
])
table.set_column_width("name", 280)
current_width = table.get_column_width("name")
table.set_column_width_mode("fill")
current_mode = table.get_column_width_mode()
table.refresh()Style Method Examples
current_style = table.get_style()
table.configure_style(
header_bg="#111827",
header_text_color="#ffffff",
selected_bg="#2563eb",
)
table.set_style(
{
"surface_bg": "#ffffff",
"row_bg": "#ffffff",
"row_alt_bg": "#f8fafc",
"text_color": "#111827",
}
)Search, Sort, and Filter Method Examples
table.sort_by("name", ascending=True)
table.search("north")
table.filter("north")
table.set_column_filter("status", {"type": "equals", "value": "Open"})
table.clear_column_filter("status")
table.clear_column_filters()Loading Method Examples
table.set_loading(True)
table.set_data(fetch_rows())
table.set_loading(False)
table.set_error("Could not load rows")
table.clear_error()thread = table.load_async(
fetch_rows,
on_success=lambda rows: None,
on_error=lambda error: None,
clear_on_error=True,
)rows_from_cursor Reference
rows_from_cursor(cursor) converts a DB-API cursor result into dictionaries using cursor.description.
from CTkDataTable import rows_from_cursor
cursor.execute("SELECT id, name, status FROM customers")
rows = rows_from_cursor(cursor)
table.set_data(rows)| Parameter | Accepted values | Returns |
|---|---|---|
cursor |
DB-API cursor after a SELECT query has executed and cursor.description is available. |
list[dict] |
The helper calls cursor.fetchall(), so it consumes all remaining result rows. Calling it before a result-producing statement raises ValueError. Use unique SQL column names or aliases because duplicate names would target the same dictionary key.
Complete Mini App
This app combines search, sorting, badges, currency, dates, checkboxes, row actions, context menus, footer summaries, and multi-select.
from datetime import date, timedelta
import customtkinter as ctk
from CTkDataTable import CTkDataTable, TableRowEvent
app = ctk.CTk()
app.title("Work Orders")
app.geometry("1080x620")
app.grid_columnconfigure(0, weight=1)
app.grid_rowconfigure(2, weight=1)
toolbar = ctk.CTkFrame(app, corner_radius=0)
toolbar.grid(row=0, column=0, sticky="ew", padx=14, pady=(14, 8))
toolbar.grid_columnconfigure(0, weight=1)
search = ctk.CTkEntry(toolbar, placeholder_text="Search work orders")
search.grid(row=0, column=0, sticky="ew", padx=(10, 8), pady=10)
detail = ctk.CTkLabel(app, text="No row selected", anchor="w")
detail.grid(row=1, column=0, sticky="ew", padx=14, pady=(0, 8))
today = date.today()
columns = [
{"key": "id", "title": "WO", "width": 90},
{"key": "title", "title": "Title", "width": 260},
{
"key": "priority",
"title": "Priority",
"width": 120,
"type": "badge",
"badge_colors": {"High": "#ef4444", "Medium": "#f59e0b", "Low": "#22c55e"},
"badge_fallback_color": "#64748b",
},
{"key": "cost", "title": "Cost", "width": 110, "type": "currency"},
{"key": "due", "title": "Due", "width": 125, "type": "date", "date_format": "%d %b"},
{"key": "complete", "title": "Done", "width": 90, "type": "checkbox"},
{
"key": "actions",
"title": "Actions",
"width": 170,
"type": "action",
"sortable": False,
"actions": [
{"key": "view", "label": "View"},
{"key": "delete", "label": "Delete"},
],
},
]
rows = [
{
"id": "WO-001",
"title": "Replace intake filter",
"priority": "High",
"cost": 450,
"due": today + timedelta(days=2),
"complete": False,
"actions": None,
},
{
"id": "WO-002",
"title": "Update inspection checklist",
"priority": "Low",
"cost": 120,
"due": today + timedelta(days=8),
"complete": True,
"actions": None,
},
{
"id": "WO-003",
"title": "Audit calibration records",
"priority": "Medium",
"cost": 275,
"due": today + timedelta(days=4),
"complete": False,
"actions": None,
},
]
table: CTkDataTable | None = None
def select_row(event: TableRowEvent) -> None:
detail.configure(text=f"Selected {event.row['id']}: {event.row['title']}")
def handle_action(event: TableRowEvent) -> None:
if table is None:
return
if event.action_key == "view":
detail.configure(text=f"Viewing {event.row['id']}")
elif event.action_key == "delete":
table.delete_row_by_key("id", event.row["id"])
def handle_context(event: TableRowEvent) -> None:
if event.action_key == "copy_id":
app.clipboard_clear()
app.clipboard_append(event.row["id"])
detail.configure(text=f"Copied {event.row['id']}")
def handle_checkbox(event: TableRowEvent) -> None:
assert event.column_key is not None
detail.configure(
text=f"{event.row['id']} complete: {event.row[event.column_key]}"
)
table = CTkDataTable(
app,
columns=columns,
data=rows,
horizontal_scroll=True,
multi_select=True,
footer=True,
summaries={"id": "count", "cost": "sum"},
context_menu=[{"key": "copy_id", "label": "Copy ID"}],
on_row_click=select_row,
on_action_click=handle_action,
on_context_action=handle_context,
on_checkbox_toggle=handle_checkbox,
)
table.grid(row=2, column=0, sticky="nsew", padx=14, pady=(0, 14))
search.bind("<KeyRelease>", lambda _event: table.search(search.get()))
app.mainloop()Operational Notes, Limitations, and Troubleshooting
Keep these runtime boundaries in mind when designing an application:
- Create and mutate
CTkDataTableon the Tkinter main thread.load_async()is the supported convenience for a blocking row fetch: onlyfetch_rowsruns on its worker, while the widget update and completion callbacks return to Tk's event thread. For custom workers, send results through a thread-safe queue that the main thread polls with an already-scheduledafter; do not call Tk methods, includingafter, from the worker. - Virtualization limits Canvas work to visible body rows, but every normalized row remains in Python memory and search, filters, summaries, and sorting are client-side. For very large or remote datasets, page/filter in the database or service and call
set_data()with the current result set. - Sorting changes only the view order, not the source list returned by
get_data(). Search and column filters also change the view and prune selection for hidden rows. - Checkbox and combobox columns are the editable cell types. Other types are display/interaction types; use your own dialog/form plus
update_cell()orupdate_row()for general editing. - The widget owns in-memory state, not persistence. Save changes in
on_change/on_cell_changeor at an explicit Save boundary, use parameterized SQL, and handle commit/rollback in application code. - Row normalization and getter/event snapshots are shallow. Top-level dictionaries are copied, but nested lists/dictionaries/objects can still be shared. Do not mutate nested values unless that sharing is intentional.
- Prefer
row_keyand..._by_id()methods for durable identity. A source index changes after deletions; a view index additionally changes with search, filters, and sorting. loadinganderrorare presentation states. They do not discard existing rows unless application code callsclear(),set_data([]), or usesload_async(clear_on_error=True).
Common problems:
| Symptom or exception | Cause and fix |
|---|---|
| Cells are blank | A column key does not exactly match the row dictionary key. Check spelling, capitalization, and SQL aliases. Missing ordinary values display blank rather than creating a key. |
TypeError says rows must be mappings |
Convert tuple cursor rows with rows_from_cursor(cursor), or configure the database driver to return mapping rows. |
| Duplicate/missing row-ID error | Every row must contain a unique, hashable value for the configured row_key. Validate the whole incoming dataset before calling set_data(). |
KeyError: Unknown column key |
A method/filter references a key absent from the current normalized columns, possibly after set_columns(). |
IndexError for a row |
Source and view indices were mixed up or became stale. Use the conversion methods or stable row IDs. |
ValueError says a selected source row is not visible |
select_row() addresses source positions but only visible rows can be selected. Clear/adjust search and filters or use the current view conversion first. |
A multi-select API call raises ValueError |
Enable multi_select=True, and pass only one of add, toggle, or range_select. |
| A typed combobox will not close/save | commit_edit() returned False; show edit_validation_error, correct the value, or call cancel_edit(). |
| A checkbox/combobox does not respond | The table is read_only, the column has editable=False, or a validator rejected the proposed value. |
| A table callback did not fire after an API update | update_cell...() defaults to notify=False; pass notify=True. Whole-row replacement methods intentionally do not emit cell-change callbacks. |
| Columns are clipped | In fixed mode, enable horizontal_scroll=True, increase the viewport, hide columns, or switch to column_width_mode="fill". |
| The table does not expand with its window | Give the containing grid row/column a positive weight and place the table with sticky="nsew", or configure the equivalent pack(fill="both", expand=True). |
| Invalid style or column option fails at startup | Use the canonical reference names and documented ranges. Unknown style names, unsupported column types, invalid dimensions, duplicate column keys, and invalid combobox choices are rejected early. |
When reporting a reproducible problem, include the Python/CTkDataTable versions, operating system, appearance mode, the smallest column/data definition that fails, and the complete traceback in the issue tracker.