What is CTkDataTable?
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.
Why Use It?
CustomTkinter FriendlyDesigned to fit naturally into modern CustomTkinter applications. |
Dictionary BasedDefine columns and rows using simple Python dictionaries. |
PracticalBuilt for dashboards, admin tools, database viewers and internal systems. |
Features
- 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
Installation
pip install CTkDataTable
CTkDataTable requires Python 3.11 or newer. Pip also installs the compatible customtkinter and dropdown dependencies.
Quick Start
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()
How It Works
flowchart LR
A[Define Columns] --> B[Create Row Data]
B --> C[Pass Data to CTkDataTable]
C --> D[Render Table]
D --> E[Display Structured Data]
Column Configuration
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 |
Supported Column Types
TextFor names, labels, descriptions and general values. |
NumberFor IDs, counts, quantities and numeric data. |
BadgeFor statuses, permissions, categories and priority labels. |
Additional built-in types include percentage, currency, date, datetime, progress, link, pill-list, checkbox, combobox and action columns.
Text Column
{
"key": "name",
"title": "Name",
"width": 160,
"type": "text"
}
Number Column
{
"key": "id",
"title": "ID",
"width": 60,
"type": "number"
}
Badge Column
{
"key": "permission",
"title": "Permission",
"width": 140,
"type": "badge",
"badge_colors": {
"Admin": ("#fecaca", "#7f1d1d"),
"Manager": ("#bfdbfe", "#1e3a8a"),
"Standard": ("#e5e7eb", "#374151")
}
}
Combobox Column
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.
Editing and Saving
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.
Row Data
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.
Use Cases
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
Project Status
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.
Contributing
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.
Licence
This project is released under the MIT Licence.
Links
Release files for CTkDataTable 1.0.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| ctkdatatable-1.0.1.tar.gz | 93.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| ctkdatatable-1.0.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 167.0 kB
Release files / ctkdatatable-1.0.1.tar.gz
| Download URL | ctkdatatable-1.0.1.tar.gz |
|---|---|
| Size | 93.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
e30f31ed541f2b3de98a792780e2204fe66a0cfc09cfda0ce9d8005388260568
|
|
BLAKE2b-256 checksum How to use checksums |
ca678c4736966f7fc2f4836b8c19be3c9b445c90b5cae2bd879108e8dd5e21e9
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 9, 2026.
Transparency logRelease files / ctkdatatable-1.0.1-py3-none-any.whl
| Download URL | ctkdatatable-1.0.1-py3-none-any.whl |
|---|---|
| Size | 73.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
f8cf5aa933fda5f56621db3e75292504bbc73dba18a21bb00ee7cb3c14d24d3b
|
|
BLAKE2b-256 checksum How to use checksums |
4a5d3fe24df517dd2fa7cd5b601d274c79f176e850b9935179b72c760c12e1b4
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 9, 2026.
Transparency log