Skip to main content

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 Friendly

Designed to fit naturally into modern CustomTkinter applications.

Dictionary Based

Define columns and rows using simple Python dictionaries.

Practical

Built 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

Text

For names, labels, descriptions and general values.

Number

For IDs, counts, quantities and numeric data.

Badge

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.

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


Built for clean, practical CustomTkinter applications.

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)

Source distribution for CTkDataTable 1.0.1
File Size Uploaded
ctkdatatable-1.0.1.tar.gz 93.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for CTkDataTable 1.0.1
File Interpreter ABI Platform
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 log

Release 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

Release history Release notifications | RSS feed

This release

1.0.1 This release

2 release files

0.2.0

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page