CTkDataTable is a Python module for building cleaner, more practical data tables inside customtkinter desktop applications.
It was created to solve a common problem: CustomTkinter is great for modern desktop interfaces, but displaying structured table data can still be awkward. Standard Tkinter options such as ttk.Treeview often feel dated, difficult to style, or out of place in a modern UI.
CTkDataTable provides a configurable table widget designed for internal tools, dashboards, admin panels, database applications and workflow software.
|
Designed to fit naturally into modern CustomTkinter applications. |
Define columns and rows using simple Python dictionaries. |
Built for dashboards, admin tools, database viewers and internal systems. |
- Built for
customtkinter - Simple column configuration
- Row data passed as dictionaries
- Configurable column titles
- Configurable column widths
- Text columns
- Number columns
- Badge columns
- Contrast-aware badge and pill labels
- Table-style combobox columns with scrollable, searchable popups
- Stable row IDs and structured edit events
- Per-column and table-wide validation
- Read-only tables and columns
- Cleaner alternative to
ttk.Treeview - Resizable columns
- Fill-width layouts
- Horizontal scrolling
- Compact, comfortable and spacious density presets
- Hover, pressed, keyboard-focus and truncation-tooltip feedback
- Useful for desktop dashboards and database-driven apps
pip install CTkDataTableCTkDataTable requires Python 3.11 or newer. Pip also installs the compatible customtkinter and dropdown dependencies.
import customtkinter as ctk
from CTkDataTable import CTkDataTable
app = ctk.CTk()
app.title("CTkDataTable Example")
app.geometry("900x500")
columns = [
{
"key": "id",
"title": "ID",
"width": 50,
"type": "number"
},
{
"key": "first_name",
"title": "First Name",
"width": 140,
"type": "text"
},
{
"key": "last_name",
"title": "Last Name",
"width": 140,
"type": "text"
},
{
"key": "position",
"title": "Position",
"width": 180,
"type": "text"
},
{
"key": "permission",
"title": "Permission",
"width": 140,
"type": "badge",
"badge_colors": {
"Admin": ("#fecaca", "#7f1d1d"),
"Manager": ("#bfdbfe", "#1e3a8a"),
"Standard": ("#e5e7eb", "#374151")
}
}
]
rows = [
{
"id": 1,
"first_name": "Harry",
"last_name": "Gomm",
"position": "Manager",
"permission": "Manager"
},
{
"id": 2,
"first_name": "Ben",
"last_name": "Jones",
"position": "Engineer",
"permission": "Standard"
},
{
"id": 3,
"first_name": "Charlie",
"last_name": "Smith",
"position": "Admin",
"permission": "Admin"
}
]
table = CTkDataTable(
master=app,
columns=columns,
data=rows,
row_key="id",
column_width_mode="fill",
resizable_columns=True
)
table.pack(fill="both", expand=True, padx=20, pady=20)
app.mainloop()flowchart LR
A[Define Columns] --> B[Create Row Data]
B --> C[Pass Data to CTkDataTable]
C --> D[Render Table]
D --> E[Display Structured Data]
Columns are defined using dictionaries.
columns = [
{
"key": "first_name",
"title": "First Name",
"width": 140,
"type": "text"
}
]| Property | Description |
|---|---|
key |
The key used to match data from each row |
title |
The text displayed in the table header |
width |
The width of the column |
type |
The column display type |
|
For names, labels, descriptions and general values. |
For IDs, counts, quantities and numeric data. |
For statuses, permissions, categories and priority labels. |
Additional built-in types include percentage, currency, date, datetime, progress, link, pill-list, checkbox, combobox and action columns.
{
"key": "name",
"title": "Name",
"width": 160,
"type": "text"
}{
"key": "id",
"title": "ID",
"width": 60,
"type": "number"
}{
"key": "permission",
"title": "Permission",
"width": 140,
"type": "badge",
"badge_colors": {
"Admin": ("#fecaca", "#7f1d1d"),
"Manager": ("#bfdbfe", "#1e3a8a"),
"Standard": ("#e5e7eb", "#374151")
}
}Combobox cells keep a dropdown arrow visible inside the table and open a themed, scrollable CTkScrollableDropdownPP popup. Lists with more than ten choices get search automatically. Options may store the same string they display, or use ComboOption to separate a friendly label from the value saved to your database.
from CTkDataTable import CellChangeEvent, ComboOption
def status_changed(event: CellChangeEvent) -> None:
print(event.row_id, event.old_value, "->", event.new_value)
{
"key": "status",
"title": "Status",
"width": 180,
"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,
"on_change": status_changed
}Set searchable=True or False to override automatic search, and use items_per_page for very long lists. Set allow_custom=True to accept typed values. Pressing Enter or clicking elsewhere commits custom text, while Escape cancels it. With allow_empty=True, the labelled empty choice and Delete/Backspace use empty_value; leaving its default as None maps naturally to SQL NULL. Existing values outside the configured choices remain visible until the user changes them. Installing CTkDataTable installs the dropdown dependency automatically.
Give database-backed tables a stable identity with row_key="id". Every row must then contain a unique, hashable ID. CellChangeEvent.row_id remains the record identity even when sorting or filtering changes the row's visible position.
get_data(), get_cell(), and the other getters are pure reads: they never commit an editor as a side effect. Put the edit boundary in your Save button:
def save_to_database() -> None:
if not table.commit_edit():
print("Cannot save:", table.edit_validation_error)
return
rows = table.get_data()
# Run parameterized INSERT/UPDATE statements, then commit your transaction.Use cancel_edit() to discard the active typed edit. For targeted access, call get_cell(source_index, "status"), get_cell_by_id(row_id, "status"), update_cell(...), or update_cell_by_id(...). Programmatic updates validate but are quiet by default; pass notify=True to emit a CellChangeEvent with origin="api". Stable-ID row helpers include get_row_by_id(), update_row_by_id(), and delete_row_by_id().
Editable checkbox and combobox columns accept editable=False, validator=..., and on_change=.... The table accepts read_only=True, cell_validator=..., and on_cell_change=.... Validators receive a CellEditRequest and return None to accept or an error message to reject. A rejected typed edit stays open so the user can correct it.
For each successful change, the model and view update first, then the column's on_change, then the table's on_cell_change. CellChangeEvent includes widget, row_id, a read-only row snapshot, source_index, view_index_before, view_index_after, column_key, old_value, new_value, and origin. on_selection_change similarly receives a SelectionChangeEvent with current, added, and removed rows/IDs/indices. Selection can also be controlled with select_row(), select_row_by_id(), and clear_selection(). The existing on_checkbox_toggle(TableRowEvent) callback remains supported for compatibility and runs after the new cell-change callbacks.
The widget intentionally does not own a database connection or transaction. If a SQL save fails, keep your original rows or reload them with set_data() after rolling back.
Rows are passed as a list of dictionaries.
rows = [
{
"id": 1,
"first_name": "Harry",
"last_name": "Gomm",
"position": "Manager",
"permission": "Manager"
}
]Each row key should match the key value defined in the column configuration.
CTkDataTable can be used for:
- Admin panels
- User management screens
- Database viewers
- Desktop dashboards
- CRUD applications
- Job management tools
- Stock or asset registers
- Reporting interfaces
CTkDataTable is available on PyPI for production use and actively maintained through practical use in real CustomTkinter desktop applications. Review the changelog when upgrading. Feedback, issues and suggestions are welcome.
Contributions are welcome.
If you find a bug, have an idea for a feature, or want to improve the documentation, feel free to open an issue or submit a pull request.
This project is released under the MIT Licence.