A virtualized data table widget for CustomTkinter applications.
Project description
CTkDataTable
The CTkDataTable package provides a virtualized data table widget for CustomTkinter apps.
Use it when you want to show records from Python data, PostgreSQL queries, SQLite queries, or SQLAlchemy results without building a grid of labels by hand.
Install
From PyPI:
pip install CTkDataTable
For local development:
pip install -e ".[dev]"
Run a Demo
python -m CTkDataTable.examples.basic_table
python -m CTkDataTable.examples.ncr_records
Development Checks
python -m unittest discover -v
python -m ruff check .
python -m mypy CTkDataTable
python -m build
python -m twine check dist/*
Release Build
Remove-Item -Recurse -Force dist, build -ErrorAction SilentlyContinue
python -m build
python -m twine check dist/*
License
MIT License. See LICENSE.
Quick Start
Using the table has three steps:
- Define the columns.
- Pass rows.
- Place the table in your CustomTkinter layout.
import customtkinter as ctk
from CTkDataTable import CTkDataTable
app = ctk.CTk()
app.geometry("700x400")
app.grid_rowconfigure(0, weight=1)
app.grid_columnconfigure(0, weight=1)
columns = [
{"key": "id", "title": "ID", "width": 80},
{"key": "name", "title": "Name", "width": 180},
{"key": "status", "title": "Status", "width": 140},
]
rows = [
{"id": 1, "name": "Alice", "status": "Open"},
{"id": 2, "name": "Bob", "status": "Closed"},
{"id": 3, "name": "Charlie", "status": "In Review"},
]
table = CTkDataTable(app, columns=columns, data=rows)
table.grid(row=0, column=0, sticky="nsew", padx=16, pady=16)
app.mainloop()
The important rule is simple:
column["key"] == row dictionary key
For example:
{"key": "name", "title": "Name", "width": 180}
{"name": "Alice"}
Loading Data
Most users can pass a list of dictionaries:
table.set_data([
{"id": 1, "name": "Alice", "status": "Open"},
{"id": 2, "name": "Bob", "status": "Closed"},
])
The table also accepts common database row objects, including:
- PostgreSQL rows returned as dictionaries
- SQLAlchemy mapping rows
sqlite3.Rowobjects- plain DB-API tuple cursors converted with
rows_from_cursor()
PostgreSQL with psycopg 3
Use dict_row, then pass the query result directly to the table.
import psycopg
from psycopg.rows import dict_row
with psycopg.connect(DB_URL, row_factory=dict_row) as connection:
rows = connection.execute("""
SELECT id, name, status
FROM customers
""").fetchall()
table.set_data(rows)
PostgreSQL with psycopg2
Use RealDictCursor.
import psycopg2
from psycopg2.extras import RealDictCursor
connection = psycopg2.connect(DB_URL)
cursor = connection.cursor(cursor_factory=RealDictCursor)
cursor.execute("""
SELECT id, name, status
FROM customers
""")
table.set_data(cursor.fetchall())
Plain DB-API Cursors
If your cursor returns tuples, convert them with rows_from_cursor().
from CTkDataTable import rows_from_cursor
cursor.execute("""
SELECT id, name, status
FROM customers
""")
table.set_data(rows_from_cursor(cursor))
If database column names do not match your table column keys, use SQL aliases:
SELECT created_at AS created FROM customers;
Column Types
Every column needs at least a key, title, and width.
{"key": "name", "title": "Name", "width": 180}
You can also set a type.
Text
{"key": "name", "title": "Name", "width": 180, "type": "text"}
Number
{"key": "amount", "title": "Amount", "width": 120, "type": "number"}
Format numbers with number_format:
{"key": "amount", "title": "Amount", "width": 120, "type": "number", "number_format": "${:,.2f}"}
Percentage
Use percentage for percent values. It right-aligns by default, sorts numerically, and appends %.
{"key": "margin", "title": "Margin", "width": 120, "type": "percentage"}
Customize output with percentage_format. If your row values are stored as ratios, multiply them before display:
{
"key": "margin",
"title": "Margin",
"width": 120,
"type": "percentage",
"percentage_format": "{value:.1f}%",
"percentage_multiplier": 100,
}
Currency
Use currency for money values. It right-aligns by default, sorts numerically, and formats numeric row values.
{
"key": "amount",
"title": "Amount",
"width": 120,
"type": "currency",
"currency_symbol": "$",
}
Customize output with currency_format and currency_negative_format:
{
"key": "amount",
"title": "Amount",
"width": 120,
"type": "currency",
"currency_symbol": "GBP ",
"currency_format": "{symbol}{value:,.2f}",
"currency_negative_format": "({symbol}{value:,.2f})",
}
Date
{"key": "created", "title": "Created", "width": 130, "type": "date"}
Date columns accept datetime.date, datetime.datetime, and ISO date strings.
Badge
Use badges for status-like values.
{
"key": "status",
"title": "Status",
"width": 130,
"type": "badge",
"badge_colors": {
"Open": "#2ecc71",
"Closed": "#e74c3c",
"Overdue": "#e67e22",
},
"badge_fallback_color": "#64748b",
}
Pill List
Use pill_list for compact tag lists. Row values can be a list, tuple, set, or a comma-separated string.
{
"key": "tags",
"title": "Tags",
"width": 180,
"type": "pill_list",
"pill_colors": {"Urgent": "#ef4444", "Finance": "#0ea5e9"},
"pill_fallback_color": "#64748b",
"pill_text_color": "#ffffff",
}
Checkbox
Checkbox columns display boolean row values and toggle them when clicked. Toggle callbacks receive a
TableRowEvent with the updated row, column_key set to the checkbox column, and action_key set to
"checkbox".
from CTkDataTable import TableRowEvent
def handle_checkbox(event: TableRowEvent) -> None:
approved = event.row[event.column_key]
columns = [
{"key": "approved", "title": "Approved", "width": 100, "type": "checkbox"},
]
table = CTkDataTable(app, columns=columns, data=rows, on_checkbox_toggle=handle_checkbox)
Progress
Use progress for numeric completion values. Values are clamped between progress_min and progress_max.
{
"key": "completion",
"title": "Complete",
"width": 140,
"type": "progress",
"progress_min": 0,
"progress_max": 100,
"progress_text_format": "{percent:.0f}%",
}
Link
Use link for clickable text cells. Link clicks fire on_link_click with a TableRowEvent.
from CTkDataTable import TableRowEvent
def handle_link(event: TableRowEvent) -> None:
selected_profile = event.row
columns = [
{"key": "profile", "title": "Profile", "width": 130, "type": "link"},
]
table = CTkDataTable(app, columns=columns, data=rows, on_link_click=handle_link)
Actions
Action columns draw buttons inside each row.
from CTkDataTable import CTkDataTable, TableRowEvent
def handle_action(event: TableRowEvent) -> None:
selected_action = event.action_key
columns = [
{"key": "id", "title": "ID", "width": 80},
{
"key": "actions",
"title": "Actions",
"width": 160,
"type": "action",
"sortable": False,
"actions": [
{"key": "view", "label": "View"},
{"key": "delete", "label": "Delete"},
],
},
]
table = CTkDataTable(
app,
columns=columns,
data=rows,
on_action_click=handle_action,
)
Common Tasks
Replace All Rows
table.set_data(rows)
Add One Row
table.add_row({"id": 4, "name": "Diana", "status": "Open"})
Search from a Search Box
search = ctk.CTkEntry(app, placeholder_text="Search")
search.grid(row=0, column=0, sticky="ew")
table.grid(row=1, column=0, sticky="nsew")
search.bind("<KeyRelease>", lambda _event: table.search(search.get()))
Sort by a Column
table.sort_by("name", ascending=True)
Users can also click sortable column headers.
Resize Columns
table = CTkDataTable(app, columns=columns, data=rows, resizable_columns=True)
Users can drag header dividers to resize columns. The table keeps the resize in memory for the current widget instance.
You can also change columns from code:
table.set_column_width("name", 220)
table.set_columns([
{"key": "id", "title": "ID", "width": 80},
{"key": "name", "title": "Customer", "width": 220},
])
Style the Table
Use style to control the table surface, header, rows, selection, dividers, feature cells, padding, and corner radii.
Pass either a dictionary or a TableStyle object.
table = CTkDataTable(
app,
columns=columns,
data=rows,
style={
"corner_radius": 12,
"border_width": 1,
"border_color": "#d1d5db",
"header_bg": "#f3f4f6",
"row_bg": "#ffffff",
"row_alt_bg": "#f9fafb",
"hover_bg": "#eef6ff",
"selected_bg": "#2563eb",
"selected_text_color": "#ffffff",
"divider_color": "#e5e7eb",
"badge_radius": 8,
"action_radius": 6,
"cell_padding_x": 14,
},
)
table.configure_style(header_bg="#111827", header_text_color="#ffffff")
table.set_style(row_bg="#ffffff", row_alt_bg="#f8fafc")
Style Rows and Cells
Style hooks are opt-in. Passing row_style or cell_style without enable_style_hooks=True raises a clear error.
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,
)
Filter by Column
Column filters combine with global search.
table.set_column_filter("status", {"type": "equals", "value": "Open"})
table.set_column_filter("amount", {"type": "range", "min": 100, "max": 500})
table.clear_column_filter("status")
table.clear_column_filters()
Supported filter types are contains, equals, not_equals, in, bool, range, and date_range.
Add a Context Menu
def handle_context(event: TableRowEvent) -> None:
selected_action = event.action_key
table = CTkDataTable(
app,
columns=columns,
data=rows,
context_menu=[
{"key": "copy_id", "label": "Copy ID"},
{"key": "delete", "label": "Delete"},
],
on_context_action=handle_context,
)
Show a Footer Summary
Footer summaries use the current visible rows after search and column filters.
table = CTkDataTable(
app,
columns=columns,
data=rows,
footer=True,
summaries={
"id": "count",
"amount": "sum",
"status": lambda rows: f"{len(rows)} visible",
},
)
Built-in summaries are count, sum, avg, min, and max.
Load Rows in the Background
def fetch_rows():
return database.load_customers()
table.load_async(
fetch_rows,
on_success=lambda rows: None,
on_error=lambda error: None,
)
load_async() shows the loading state, runs your fetch function in a background thread, and updates the table safely on the Tkinter thread.
Get the Selected Row
selected = table.get_selected_row()
if selected is not None:
selected_id = selected.get("id")
For row identity, use source-data indices or current view indices:
source_indices = table.get_selected_indices()
view_indices = table.get_selected_view_indices()
Delete Rows
table.delete_row(0)
table.delete_view_row(0)
table.delete_row_by_key("id", 4)
table.delete_selected_rows()
delete_row(index) uses the original source-data index. delete_row_by_key() is usually easier after sorting or filtering.
Use delete_view_row(view_index) when you intentionally want to target the current visible row order.
Detailed Event Payloads
Interaction callbacks receive TableRowEvent objects when you need the row, source index, visible index, clicked column, or clicked action.
def handle_action(event):
row_identity = (event.source_index, event.view_index, event.action_key)
table = CTkDataTable(
app,
columns=columns,
data=rows,
on_action_click=handle_action,
)
For link cells, event.column_key is the link column and event.action_key is "link".
For checkbox cells, event.column_key is the checkbox column, event.action_key is "checkbox", and
event.row[event.column_key] is the new boolean value.
Show a Loading State
table.set_loading(True)
table.set_data(rows)
table.set_loading(False)
Enable Horizontal Scrolling
Use this when the total column width is wider than the window.
table = CTkDataTable(app, columns=columns, data=rows, horizontal_scroll=True)
Keyboard Navigation
When the table has focus, use Up, Down, Page Up, Page Down, Home, and End to move selection. Press Enter to trigger the double-click row callback. In multi-select mode, Shift extends the selection range.
Notes
- The table does not edit text, number, date, or badge cells inline. Checkbox columns can toggle boolean values.
- The table does not run database queries. Query your database yourself, then call
set_data(). - The table only draws visible rows, so large lists scroll smoothly.
delete_row(index)deletes from the original data list, not the currently visible filtered or sorted row number.- Searching clears selected rows that are no longer visible.
- Sorting and searching reset the vertical scroll position to the top.
More Detail
- Full API reference: docs/Docs.md
- Publishing guide: docs/Publishing.md
- Standalone HTML guide: docs/Docs.html
- Basic example: CTkDataTable/examples/basic_table.py
- NCR records example: CTkDataTable/examples/ncr_records.py
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file ctkdatatable-0.1.0.tar.gz.
File metadata
- Download URL: ctkdatatable-0.1.0.tar.gz
- Upload date:
- Size: 46.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5f35bfb854e90863969bd8fff27a60be60b1e8515579fa06036c1f5d404ac1d6
|
|
| MD5 |
289a3310f2d6dffdf72df7eddcb2b0fd
|
|
| BLAKE2b-256 |
bc0a36af999fb9efca5175ede91a426142b6ef65f1528140f0b51e3600f9b611
|
Provenance
The following attestation bundles were made for ctkdatatable-0.1.0.tar.gz:
Publisher:
publish.yml on Harry-g25/CTkTableData
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
ctkdatatable-0.1.0.tar.gz -
Subject digest:
5f35bfb854e90863969bd8fff27a60be60b1e8515579fa06036c1f5d404ac1d6 - Sigstore transparency entry: 1740127369
- Sigstore integration time:
-
Permalink:
Harry-g25/CTkTableData@c35475f67a6d1c8241b1daa84b3f4e958f838592 -
Branch / Tag:
refs/tags/0.1.0 - Owner: https://github.com/Harry-g25
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@c35475f67a6d1c8241b1daa84b3f4e958f838592 -
Trigger Event:
release
-
Statement type:
File details
Details for the file ctkdatatable-0.1.0-py3-none-any.whl.
File metadata
- Download URL: ctkdatatable-0.1.0-py3-none-any.whl
- Upload date:
- Size: 43.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4b2d86e4025c0c2821be15eb1701e4485e50f1e38c5db9ae64692721570189f4
|
|
| MD5 |
dc040c0d4c05757def85941246feff06
|
|
| BLAKE2b-256 |
bb8093cb2fa552061fb90a7fc07520aa6818358f2116ab049f1808d572bf2af0
|
Provenance
The following attestation bundles were made for ctkdatatable-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on Harry-g25/CTkTableData
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
ctkdatatable-0.1.0-py3-none-any.whl -
Subject digest:
4b2d86e4025c0c2821be15eb1701e4485e50f1e38c5db9ae64692721570189f4 - Sigstore transparency entry: 1740127389
- Sigstore integration time:
-
Permalink:
Harry-g25/CTkTableData@c35475f67a6d1c8241b1daa84b3f4e958f838592 -
Branch / Tag:
refs/tags/0.1.0 - Owner: https://github.com/Harry-g25
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@c35475f67a6d1c8241b1daa84b3f4e958f838592 -
Trigger Event:
release
-
Statement type: