CTkKanban
CTkKanban is a small Kanban widget for CustomTkinter. It focuses on predictable card editing and movement instead of trying to be a database framework or a complete project-management application.
It provides:
- a
CTkKanbanBoardwidget with explicit editing, search, selection, menus, handle-only dragging, loading states, and light/dark theme support; - a schema-driven editor for any number of typed card fields;
- granular action permissions, layout configuration, and customizable text;
- a Tk-free
BoardModelwith validation and deterministic manual ordering; - detached snapshots, database-row adapters, and one application-owned change boundary for persistence.
The complete single-page guide is also available in
docs/index.html. This README covers the same contracts in
a format suited to package and repository viewers.
Contents
- Install
- Quick start
- Runnable examples
- Interaction model
- Styling and theme tokens
- Configuration and permissions
- Configurable card fields
- Data contract and ordering
- Change events and persistence
- Database rows
- Asynchronous loading
- API reference
- Errors and lifecycle
- Migrating from 1.x
- Development
Install
python -m pip install CTkKanBan
CTkKanban requires Python 3.10 or newer and installs CustomTkinter 5.2.2 or
newer (below major version 7). CTkKanBan is the PyPI distribution name;
ctk_kanban is the lowercase Python package name.
Quick start
Import the package with its canonical lowercase name:
import customtkinter as ctk
from ctk_kanban import CTkKanbanBoard
app = ctk.CTk()
app.geometry("1000x650")
board = CTkKanbanBoard(
app,
columns=[
{"id": "todo", "title": "To do"},
{"id": "doing", "title": "Doing"},
{"id": "done", "title": "Done"},
],
cards=[
{
"id": 1,
"column": "todo",
"title": "Try the simplified board",
"description": "Click for details and use the handle to drag.",
"priority": "High",
"tags": ["demo"],
}
],
on_change=lambda event: print(event["type"], event["data"]),
)
board.pack(fill="both", expand=True)
app.mainloop()
columns and cards may be any iterables of mappings. Their input order is
the initial manual order. Call board.get_data() at any time to retrieve the
complete, detached state.
Runnable examples
Start with the smallest example for your use case. Every program can be run
directly from the repository root after python -m pip install -e ".[dev]".
| Goal | Example | What it demonstrates |
|---|---|---|
| Build a normal board | examples/basic_board.py |
Minimal data, equal-width columns, editing, dragging, search, and on_change. |
| Put database fields on cards | examples/custom_fields.py |
Field, CardField, custom title keys, all generated input types, display roles, validation, formatting, and visibility. |
| Load and save real rows | examples/sqlite_board.py |
In-memory SQLite, cursor loading, source-key mappings, typed columns, and persistence after edits or moves. |
| Own the card form | examples/custom_editor.py |
use_builtin_editor=False, on_card_open(card), a custom window, and update_card(). |
| Fetch without freezing Tk | examples/async_loading.py |
snapshot_from_rows(), load_async(), loading state, and success/error delivery. |
| Explore everything together | example.py |
Low-level field mappings, runtime schema replacement, permissions, layout, text, theme tokens, and snapshots. |
The focused examples favor copyable application patterns; the larger showcase
is useful for visually exploring the full board. See
examples/README.md for the same index and run command.
Interaction model
- Click a card to select it and open the editor drawer.
- Save explicitly with Save changes or Enter; Escape cancels.
- Drag cards only from their upper-right drag handle.
- Use the visible menu for move and delete actions.
- Columns use menu actions for left/right movement instead of column dragging.
There is no inline editing, click-away autosave, whole-card dragging, floating drag preview, or window-wide drag binding. A local Tk grab makes sure a handle drag always receives its release event.
The drawer marks changed state and disables Save until a value differs. Enter
saves from ordinary controls, Ctrl+Enter saves from anywhere in the drawer,
and Escape cancels. Enter inside a multiline textbox inserts a new
line. Opening another editor replaces the currently open drawer.
Search is a case-insensitive substring match across only fields marked
searchable; lists contribute each item. Search changes visibility, not data.
Dragging and menu up/down ordering are disabled while results are filtered,
because a visible index is ambiguous relative to hidden cards. Clear the query
before interactive reordering.
Styling and theme tokens
Board surfaces, controls, text, hover states, and scrollbars follow the active
CustomTkinter color theme. Call ctk.set_default_color_theme(...) before
creating the board to use another built-in or custom theme. Priority and tag
metadata remain visible as compact colored pills. The optional theme mapping
can override individual board tokens when needed. DEFAULT_THEME contains the
complete supported token set and unknown keys are rejected to catch spelling
mistakes.
Themes now cover colors as well as component radii, borders, spacing, control heights, scrollbar width, compact-card limits, animation timing, and font definitions:
board = CTkKanbanBoard(
app,
columns=columns,
cards=cards,
theme={
"card_corner_radius": 16,
"card_title_font": {"size": 15, "weight": "bold"},
"card_description_max_chars": 220,
"column_gap": 10,
"editor_section_corner_radius": 14,
},
)
Visual choices belong in theme; behavior, layout, and user-facing labels
belong in config.
Call merge_theme(overrides) when a reusable, validated mapping is useful.
It reads fresh defaults from the active CustomTkinter theme, deep-copies the
overrides where possible, and rejects unknown token names. DEFAULT_THEME is
the import-time discovery snapshot. A color may be a normal CustomTkinter
color string or a (light, dark) pair. Font tokens are keyword mappings passed
to ctk.CTkFont. Palette tokens such as column_accent_colors and
tag_pill_colors must remain non-empty sequences.
Complete token reference
The supported token set is intentionally closed. Values are shown through
DEFAULT_THEME; the table below describes all 97 names.
| Area | Tokens |
|---|---|
| Board surfaces | board_fg_color, toolbar_fg_color, column_fg_color, column_header_fg_color, column_border_color, column_accent_colors |
| Cards and drag feedback | card_fg_color, card_hover_color, dragging_card_fg_color, card_border_color, selected_border_color, drop_indicator_color |
| Shared colors | text_color, muted_text_color, accent_color, control_hover_color, count_fg_color, empty_icon_fg_color, divider_color, danger_color |
| Editor/input/scroll colors | editor_fg_color, editor_section_fg_color, input_border_color, scrollbar_color, scrollbar_hover_color, error_text_color |
| Pills and priorities | pill_text_color, priority_low_color, priority_medium_color, priority_high_color, priority_critical_color, tag_pill_colors |
| Native context menu | menu_fg_color, menu_text_color, menu_hover_color, menu_disabled_text_color |
| Board and toolbar geometry | board_padding_x, board_padding_y, toolbar_height, toolbar_corner_radius, toolbar_padding_x, toolbar_padding_y, toolbar_content_padding_y, search_width, button_height, control_corner_radius, small_control_size |
| Toolbar fonts | toolbar_title_font, toolbar_summary_font |
| Column geometry | column_corner_radius, column_border_width, column_gap, column_accent_height, column_header_padding_x, card_gap |
| Column fonts | column_title_font, column_count_font, column_empty_title_font, column_empty_body_font |
| Card geometry and limits | card_corner_radius, card_border_width, card_selected_border_width, card_accent_width, card_description_max_chars, card_max_visible_tags, pill_height, pill_corner_radius |
| Card fonts | card_title_font, card_body_font, card_metadata_font, pill_font |
| Editor layout | editor_border_width, editor_header_padding_x, editor_header_padding_y, editor_form_padding_x, editor_form_padding_y, editor_field_padding_x, editor_field_gap, editor_section_gap, editor_section_corner_radius, editor_section_border_width, editor_section_title_padding_y |
| Editor motion | editor_slide_step, editor_slide_interval_ms |
| Editor fonts | editor_eyebrow_font, editor_title_font, editor_status_font, section_title_font, field_label_font, help_text_font, status_text_font |
| Inputs and scrollbar | input_height, compact_input_height, input_corner_radius, input_border_width, textbox_height, scrollbar_width |
Theme mappings are resolved at construction time. Changing the source mapping later does not restyle existing child widgets; create or rebuild the board when switching to a substantially different per-board theme.
Configuration and permissions
config accepts a BoardConfig instance or a nested mapping. It separates
available actions, major layout choices, labels, and delete confirmation:
board = CTkKanbanBoard(
app,
columns=columns,
cards=cards,
config={
"actions": {
"delete_cards": False,
"delete_columns": False,
"move_columns": False,
},
"layout": {
"show_toolbar": True,
"fill_columns": True,
"use_builtin_editor": True,
"column_width": 340,
"column_height": 560,
"editor_width": 480,
},
"text": {
"board_title": "Release planning",
"add_card": "+ New work item",
},
"confirm_delete": True,
},
)
The same configuration can be constructed with frozen dataclasses:
from ctk_kanban import ActionConfig, BoardConfig, LayoutConfig, TextConfig
config = BoardConfig(
actions=ActionConfig(delete_cards=False, delete_columns=False),
layout=LayoutConfig(column_width=340, column_height=560),
text=TextConfig(board_title="Release planning"),
confirm_delete=True,
)
Nested mappings may be partial; omitted values retain the defaults. Unknown
keys are rejected. merge_config(config) validates either representation and
returns a BoardConfig.
Action settings
All action settings default to True.
| Setting | Affected board behavior |
|---|---|
add_cards |
Add-card controls and add_card(); the add-card editor becomes a no-op. |
edit_cards |
Card opening/edit controls and update_card(); on_card_open is not called when editing is disabled. |
move_cards |
Dragging, card move menus, editor column changes, move_card(), and update_card() calls that change column. |
delete_cards |
Card delete menus and delete_card(); also blocks cascading deletion through a non-empty column. |
add_columns |
Add-column controls/dialog and add_column(). |
edit_columns |
Column rename controls and update_column(). |
move_columns |
Column left/right controls and move_column(). |
delete_columns |
Column delete controls and delete_column(). |
Disabled public mutation methods raise BoardModelError. UI-opening helpers
return without opening when their action is disabled. These settings do not
block set_data(), set_fields(), or direct access to board.model; they are
widget behavior controls, not a security boundary. BoardModel deliberately
has no action configuration.
When card deletion is disabled, delete_column(..., delete_cards=True) is
also blocked for non-empty columns. This prevents column deletion from
bypassing the card policy. confirm_delete only controls confirmation dialogs
started by the built-in menus; direct API deletions never prompt.
Layout settings
| Setting | Default | Contract |
|---|---|---|
show_toolbar |
True |
Show the title, summary, search, and add buttons. search() remains usable when hidden. |
enable_drag |
True |
Enable handle-only card dragging when move_cards is also enabled. Menu/API movement remains available when only dragging is off. |
use_builtin_editor |
True |
Use the generated drawer. When false, on_card_open(card) can handle card clicks and add-editor opening is a no-op. |
fill_columns |
False |
Give every column an equal share of surplus horizontal space. column_width remains the minimum, so narrower boards still scroll. |
column_width |
320 |
Column width in widget pixels; integer at least 220. |
column_height |
500 |
Minimum board-column height; integer at least 240. |
editor_width |
420 |
Width of the embedded drawer; integer at least 320. |
Scrollbars use a 14-pixel default thickness and can be resized with the
scrollbar_width theme token. The horizontal range is recalculated after host
window resizes, so columns remain reachable on a narrower window. A wheel
gesture scrolls a column vertically when it has overflow; over a short column
or the board background it moves horizontally. Shift+wheel always moves the
board horizontally.
Text settings
| Setting | Default |
|---|---|
board_title |
"Board" |
search_placeholder |
"Search cards…" |
add_card |
"+ Add card" |
add_column |
"Add column" |
no_columns |
"No columns yet" |
no_columns_help |
"Create a column to start planning" |
no_cards |
"No cards yet" |
no_cards_help |
"Add a card to get started" |
no_results |
"No results" |
no_results_help |
"Try another search" |
Every text setting must be a string. These are the stable application-facing labels; short mechanical labels inside the editor and context menus are not currently configurable.
For the common deletion-only case, the convenience arguments are equivalent:
board = CTkKanbanBoard(
app,
columns=columns,
cards=cards,
allow_card_deletion=False,
allow_column_deletion=False,
)
Existing direct options such as show_toolbar, enable_drag,
use_builtin_editor, fill_columns, column_width, column_height,
editor_width, confirm_delete, and board_title override
the corresponding structured setting when explicitly supplied. The two
allow_*_deletion arguments override actions.delete_cards and
actions.delete_columns. Remaining keyword arguments are forwarded to the
outer CTkFrame after the board supplies its default fg_color.
Configurable card fields
Pass fields to define any number of typed card values. The built-in editor is
generated from these definitions, card rendering uses their display roles, and
search uses fields marked searchable. When fields is omitted, the existing
title, description, priority, and tags behavior remains unchanged.
Concise fields
Use Field for a fluent definition similar to CTkDataTable's Column builder.
A plain string is shorthand for a visible, editable, searchable text field;
its database-style key is converted to a readable label automatically.
from ctk_kanban import CardField, Field
fields = [
Field("summary").label("Task").title(),
"customer_name", # Customer Name text input and metadata pill
Field("description").textarea().body(),
Field("severity")
.select(["Low", "Medium", "High"])
.badge(colors={"High": "#EF4444"}),
CardField("due_date", label="Due", type="date"),
Field("estimate_hours").label("Estimate").integer(minimum=0),
Field("internal_notes").textarea().editor_only(),
]
CardField is the immutable typed form. Field(...).build() returns a detached
CardField. Field provides text(), textarea(), number(), integer(),
select(), multiselect(), date(), datetime(), checkbox(), and tags()
input helpers. Its display and behavior helpers are title(), body(),
badge(), metadata(), card_only(), editor_only(), hide(),
read_only(), required(), searchable(), section(), placeholder(),
help(), default(), length(), validate(), and fmt().
Exactly one field is the title. Calling .title() allows a database key such
as summary to be the heading. If no supplied field has that role, the normal
title definition is inserted automatically.
Advanced mapping definitions
Existing mapping definitions remain supported for callers that want to set each low-level schema option explicitly:
fields = [
{
"key": "title",
"label": "Title",
"type": "text",
"required": True,
"show_on_card": True,
"searchable": True,
"card_role": "title",
"section": "Details",
},
{
"key": "client",
"label": "Client",
"type": "text",
"show_on_card": True,
"searchable": True,
"card_role": "metadata",
"section": "Details",
},
{
"key": "estimate",
"label": "Estimate",
"type": "integer",
"min": 0,
"max": 100,
"show_on_card": True,
"card_role": "metadata",
"section": "Planning",
},
{
"key": "blocked",
"label": "Blocked",
"type": "checkbox",
"section": "Planning",
},
{
"key": "stage",
"label": "Stage",
"type": "select",
"options": ["Discovery", "Delivery", "Review"],
"show_on_card": True,
"card_role": "badge",
"colors": {
"Discovery": ("#DBEAFE", "#1E3A5F"),
"Delivery": ("#DCFCE7", "#14532D"),
},
},
{
"key": "due_date",
"label": "Due date",
"type": "date",
"show_on_card": True,
"card_role": "metadata",
},
]
cards = [
{
"id": 1,
"column": "todo",
"title": "Prepare proposal",
"client": "Acme",
"estimate": 8,
"blocked": False,
"stage": "Delivery",
"due_date": "2026-08-28",
}
]
board = CTkKanbanBoard(app, columns=columns, cards=cards, fields=fields)
Field types and stored values
| Type | Generated editor control | Normalized stored value |
|---|---|---|
text |
Single-line entry | Trimmed str; None becomes "". |
textarea |
Multi-line textbox | Trimmed str; None becomes "". |
number |
Entry | float, or None when blank. Booleans are rejected. |
integer |
Entry | int, or None when blank; a fractional float is rejected. |
select |
Option menu | The selected value. Non-empty options are enforced. |
multiselect |
Add/remove pill input | Deduplicated list; non-empty options are enforced on save. |
date |
Entry | ISO YYYY-MM-DD string or ""; accepts datetime.date input. |
datetime |
Entry | ISO date-time string or ""; accepts datetime.datetime and Z input. |
checkbox |
Checkbox | Strict bool. |
tags |
Add/remove tag pills | Deduplicated list[str]; values are trimmed, nonblank, and comma-free. |
hidden |
Hidden by default | Deep-copied application value; useful for schema-controlled values without UI. |
Date and date-time controls are plain text entries, so validation occurs on
save. multiselect uses the same compact add/remove interaction as tags;
invalid choices are reported when the schema validates the completed card.
Field definition reference
Every low-level mapping definition requires a unique, nonblank key and
label; Field, CardField, and string shorthand infer the label.
| Option | Default | Meaning |
|---|---|---|
type |
"text" |
One of the eleven types above. |
required |
False |
Reject None, "", and []. The title is always required. |
default |
Type default | Deep-copied when the field is absent. Lists default to [], checkbox to False, numeric fields to None, and other controls to "" in a new editor. |
placeholder |
"" |
Entry hint; list fields fall back to "Add a value". |
options |
() |
Sequence of allowed select or multiselect values. An empty sequence means unrestricted validation. |
show_on_card |
Role-dependent | Render the value on the compact card. Defaults to true unless the role is hidden. |
show_in_editor |
Type-dependent | Generate an editor control. Defaults to false only for hidden. |
searchable |
False |
Include the field in local case-insensitive substring search. |
read_only |
False |
Disable its generated control while retaining the value. Give required read-only fields a default for new cards. |
section |
"Details" |
Drawer section heading. Sections follow first appearance; column selection is in Organisation. |
card_role |
metadata or hidden |
Compact-card presentation role. A visible field defaults to metadata; otherwise hidden. |
help_text |
"" |
Supporting editor text; for checkboxes it becomes the checkbox label. |
min, max |
unset | Inclusive numeric limits for number and integer. |
min_length, max_length |
unset | Inclusive character/item count limits for strings and lists. |
validator |
unset | Callable `(value, card) -> bool |
formatter |
unset | Callable (value, card) -> str used only for compact-card display. |
colors |
{} |
Mapping from exact field values to CustomTkinter-compatible pill/accent colors. |
Unknown definition options are rejected so spelling mistakes cannot silently
change behavior. min must not exceed max, length limits must be
nonnegative, and the lower length limit must not exceed the upper one.
Validators run in field order and should be fast and side-effect free because
they also run during load and schema replacement.
def estimate_validator(value, card):
if value is not None and card.get("blocked") and value > 8:
return "Blocked work must be split into estimates of 8 or less"
return True
fields = [
{"key": "title", "label": "Title", "required": True},
{"key": "blocked", "label": "Blocked", "type": "checkbox"},
{
"key": "estimate",
"label": "Estimate",
"type": "integer",
"validator": estimate_validator,
"formatter": lambda value, _card: "" if value is None else f"{value} pts",
"show_on_card": True,
"card_role": "metadata",
},
]
Compact-card roles
| Role | Presentation |
|---|---|
title |
Main heading. Exactly one text/textarea field has this role; its key may match the source database column. |
body |
Wrapped body line, truncated by card_description_max_chars. Multiple body fields are supported. |
badge |
Colored pill; the first non-empty visible badge also colors the card's accent strip. |
tags |
One #value pill per item, capped per field by card_max_visible_tags. |
metadata |
Pill formatted as Label: value. |
hidden |
No compact representation. |
Only fields with show_on_card=True render, regardless of role. Empty values
are omitted. formatter changes display text but never the normalized stored
value. A field's colors mapping takes precedence for its exact value; the
default priority field otherwise uses the priority theme colors.
id, column, and column_id are reserved structural keys. If no supplied
field uses the title role, the default title definition is inserted. The
chosen heading must use text or textarea, is forced to required=True and
show_on_card=True, and must be the only field with card_role="title".
Additional card keys that are not in the schema are deep-copied and preserved in snapshots. This lets applications round-trip private integration metadata without showing, validating, formatting, or searching it. Add a definition whenever the board should understand such a value.
Use get_fields() to inspect the active definitions and set_fields(fields)
to replace them at runtime. set_fields() validates existing cards atomically,
rebuilds their compact views, and rebuilds an open editor with the new controls.
If schema normalization changes stored card values, one fields_changed event
is emitted so the host can persist the new snapshot.
get_fields() returns a detached copy. Mutating that copy has no effect until
it is passed back to set_fields(). A failed replacement leaves both the old
schema and every card unchanged. Changing a schema can add defaults, coerce
values, or reject existing data; plan schema migrations the same way you would
plan a database migration.
Data contract and ordering
A snapshot is exactly one mapping with columns and cards keys:
snapshot = {
"columns": [
{"id": "todo", "title": "To do"},
{"id": "done", "title": "Done"},
],
"cards": [
{"id": 1, "column": "todo", "title": "Write docs"},
],
}
Extra top-level snapshot keys and extra column keys are rejected. A column has
only id and title; titles are trimmed and must remain nonblank. A card must
have id, a column reference, and a nonblank title. Input may use column_id
as an alias for column; output always uses column. Supplying both aliases
with different values is an error.
IDs must be unique within their record kind and must be a nonblank str or an
int; booleans, floats, None, and other objects are rejected. IDs are not
coerced, so 1 and "1" are distinct. Use only strings or integers when
serializing to JSON and keep the same type in related card/column values.
With the default field schema, cards normalize title, description,
priority, and tags. Priority is case-sensitive and must be empty, Low,
Medium, High, or Critical. A custom schema controls its configured keys;
all other card keys are preserved as application metadata.
Snapshots, copies, and atomic replacement
get_data() and BoardModel.snapshot() return deep, detached state. The
record getters and returned mutation records are detached too, so changing
them never mutates the live board. Call a mutation method or set_data() to
apply a change.
set_data(snapshot) validates the entire replacement before changing the
model and redraws without emitting on_change. Invalid replacement data leaves
the old board intact. The snapshot shape must be complete; use empty lists to
clear a board. BoardModel.load() additionally supports the explicit form
load(columns=..., cards=...), but snapshot and explicit arguments cannot be
mixed.
Manual ordering
Column input order is retained. Card order is retained within each column and
global get_cards() output is grouped by current column order. index values
are zero-based insertion positions; None appends. Valid insertion positions
range from zero through the destination size, inclusive. An out-of-range or
non-integer index raises BoardModelError.
update_card() merges its mapping into the existing record; it does not
replace unspecified values. A changed column appends the card to that column,
while move_card() accepts an exact insertion index. update_column() only
accepts title. Non-empty columns require the explicit
delete_cards=True cascade flag before deletion.
Change events and persistence
Pass one on_change(event) callback to observe successful board mutations.
Every event contains a detached before snapshot, the current complete data
snapshot, its type, and the operation payload:
| Event type | Additional payload |
|---|---|
card_added |
card |
card_updated |
card, previous |
card_deleted |
card (the removed record) |
card_moved |
card, previous |
column_added |
column |
column_updated |
column, previous |
column_deleted |
column; any cascaded cards remain available in before |
column_moved |
column |
fields_changed |
fields (the new definitions), emitted only if schema normalization changed stored cards |
No event is emitted for a no-op update or move, set_data(), a schema change
that leaves card data identical, search, selection, rendering, or loading-state
changes. The board state has already changed when the callback runs. Callback
exceptions are logged and do not roll back the mutation.
def persist(event):
try:
repository.save_snapshot(event["data"])
except Exception:
# Optional application policy: restore the last detached state.
board.set_data(event["before"])
raise
board = CTkKanbanBoard(app, columns=columns, cards=cards, on_change=persist)
on_change runs on Tk's UI thread, so synchronous network or database work
will freeze the interface. For nontrivial persistence, enqueue event["data"]
to an application worker and decide how your application handles failures,
retries, coalescing, optimistic concurrency, and conflicts. CTkKanban does not
save, poll, page, retry, or resolve conflicts itself.
Database rows
Database results can be displayed without renaming every selected column.
CTkKanbanBoard.from_rows() accepts mapping rows, sqlite3.Row, SQLAlchemy
mapping rows, or executed DB-API cursors directly. Key mappings point from the
board's structural names to the source query names:
from ctk_kanban import CTkKanbanBoard, Field
board = CTkKanbanBoard.from_rows(
app,
columns=column_cursor,
cards=card_cursor,
column_keys={"id": "status_id", "title": "status_name"},
card_keys={
"id": "task_id",
"column": "status_id",
"title": "summary",
},
fields=[
"customer_name",
Field("severity").badge(),
Field("due_date").date(),
Field("estimate_hours").integer(),
],
)
board.pack(fill="both", expand=True)
Mapped structural source keys are consumed and outputs use canonical id,
column, and title. Other selected columns retain their database names.
Missing mapped columns and unknown mapping targets produce clear errors.
Plain DB-API tuple results can be converted using cursor metadata. Fetch each result before reusing its cursor:
from ctk_kanban import rows_from_cursor, snapshot_from_rows
cursor.execute("SELECT id, title FROM kanban_columns ORDER BY position")
columns = rows_from_cursor(cursor)
cursor.execute(
"""
SELECT id, column_id AS column, title, description, priority, tags
FROM kanban_cards
ORDER BY column_id, position
"""
)
cards = rows_from_cursor(cursor)
board.set_data(snapshot_from_rows(columns, cards))
SQL aliases such as column_id AS column remain a valid alternative to key
mappings. Result column names must be unique. rows_from_cursor() consumes all
remaining rows returned by the cursor.
snapshot_from_cursors(columns_cursor, cards_cursor) is a shorter equivalent
when two separately executed cursors are available. Every snapshot helper
normalizes and validates the complete result before returning it.
| Adapter | Behavior |
|---|---|
normalize_row(row) |
Copies a mapping, an object with _mapping, or a keys/index row to dict[str, Any]. Plain tuples have no names and raise TypeError. |
normalize_rows(rows) |
Applies normalize_row() to an iterable. |
rows_from_cursor(cursor) |
Requires an executed result with description, verifies unique column names, calls fetchall(), and zips each tuple to those names. |
snapshot_from_rows(columns, cards, *, fields=None, card_keys=None, column_keys=None) |
Accepts row iterables or cursors, applies optional source-key mappings, validates through a temporary BoardModel, and returns its detached snapshot. |
snapshot_from_cursors(columns_cursor, cards_cursor, *, fields=None, card_keys=None, column_keys=None) |
Consumes two separately executed cursors and delegates to snapshot_from_rows(). |
Pass the same fields used by the board so database values receive identical
normalization. SQL drivers may return JSON/array values in driver-specific
forms; convert them to the expected Python list, boolean, number, or string in
the query/repository layer when necessary. Neither adapter executes SQL,
commits, closes a cursor, nor owns a connection.
Asynchronous loading
load_async() performs fetching and validation on a daemon worker, then calls
set_data() and user callbacks safely on Tk's thread:
import psycopg
from psycopg.rows import dict_row
from ctk_kanban import snapshot_from_rows
def fetch_board():
with psycopg.connect(DATABASE_URL, row_factory=dict_row) as connection:
columns = connection.execute(COLUMN_QUERY).fetchall()
cards = connection.execute(CARD_QUERY).fetchall()
return snapshot_from_rows(columns, cards)
board.load_async(
fetch_board,
on_success=lambda snapshot: print("Loaded", len(snapshot["cards"]), "cards"),
on_error=lambda error: print("Load failed:", error),
)
board.is_loading reports pending work and board.load_error retains the most
recent asynchronous error. Existing data is preserved on failure unless
clear_on_error=True is requested. Starting a newer load makes an older result
stale, so it cannot overwrite newer data.
load_async() returns the started daemon threading.Thread. The fetch
callable receives no arguments and must return a complete snapshot mapping. It
runs off-thread and must not read or update Tk widgets. Validation uses the
board field schema; application of the validated snapshot plus on_success or
on_error happens on Tk's event loop. Successful loads use set_data() and
therefore do not emit on_change.
At the start of a load, load_error is cleared and is_loading becomes true.
On failure, the original exception is stored and passed to on_error; with
clear_on_error=True, the board is replaced with empty lists first. A newer
load invalidates delivery from older workers but does not forcibly stop their
Python threads. Destroying the board invalidates pending delivery, and starting
a load after destruction raises RuntimeError.
set_loading(True) is also public for application-owned tasks. It changes the
toolbar presentation and disables its search/add controls; it does not start a
worker, block public mutations, or alter data.
Pass on_card_open to run a function when a card is clicked. The callback
receives one detached card dictionary. Set use_builtin_editor=False when the
callback should run without CTkKanban opening its generated drawer:
def card_clicked(card):
open_my_form(card)
board = CTkKanbanBoard(
app,
columns=columns,
cards=cards,
use_builtin_editor=False,
on_card_open=card_clicked,
)
Providing on_card_open also replaces the edit drawer when
use_builtin_editor=True, preserving the original callback behavior. Use the
public add_card() and update_card() methods when your own form saves.
Card schemas and shared fonts are reused across compact cards, and search now
hides and reorders existing card widgets instead of destroying and recreating
them on every query. Initial rendering and set_data() still create real Tk
widgets on the main thread, so schemas that show many labels or pills per card
have a cost proportional to the number of visible controls.
API reference
CTkKanbanBoard constructor
CTkKanbanBoard(
master,
columns=(),
cards=(),
*,
on_change=None,
on_card_open=None,
theme=None,
fields=None,
config=None,
show_toolbar=None,
enable_drag=None,
use_builtin_editor=None,
fill_columns=None,
column_width=None,
column_height=None,
editor_width=None,
confirm_delete=None,
allow_card_deletion=None,
allow_column_deletion=None,
board_title=None,
**kwargs,
)
master is the parent Tk widget. columns and cards provide initial state;
all other inputs are keyword-only. theme, fields, and config use the
contracts above. on_change receives event dictionaries and on_card_open
receives a detached card record when a card is clicked.
Direct layout/text/deletion options take precedence over structured config.
CTkKanbanBoard.from_rows()
CTkKanbanBoard.from_rows(
master,
columns,
cards,
*,
card_keys=None,
column_keys=None,
fields=None,
**board_options,
)
This classmethod consumes supported row iterables or executed DB-API cursors,
maps database names, validates the complete snapshot, and returns a normal
CTkKanbanBoard. card_keys accepts canonical id, column, and title
targets; column_keys accepts id and title.
Board data, view, and schema methods
| Member | Return | Behavior |
|---|---|---|
get_data() |
BoardSnapshot |
Detached complete state. |
set_data(data) |
None |
Atomically validate, replace, and redraw without an event. |
get_card(card_id) |
`CardRecord | None` |
get_cards(column_id=None) |
list[CardRecord] |
Ordered detached records; an unknown explicit column raises. |
get_columns() |
list[ColumnRecord] |
Ordered detached records. |
get_fields() |
list[dict[str, Any]] |
Detached normalized definitions. |
set_fields(fields) |
None |
Atomic schema replacement, redraw, and optional fields_changed event. |
get_selected_card() |
`CardRecord | None` |
search(query) |
None |
Set case-insensitive local search; non-string values are converted with str(). |
refresh(preserve_scroll=True) |
None |
Rebuild structural widgets from current model state. Ordinary public mutations already refresh what they need. |
Board mutation methods
| Member | Return | Notes |
|---|---|---|
add_card(card, *, index=None) |
CardRecord |
Add to card["column"] at an insertion position. |
update_card(card_id, updates) |
CardRecord |
Merge fields. Changing column additionally requires move permission. |
move_card(card_id, column_id, index=None) |
CardRecord |
Move/reorder, appending when index is omitted. |
delete_card(card_id) |
CardRecord |
Remove and return the old record; no confirmation prompt. |
add_column(column, *, index=None) |
ColumnRecord |
Add at an insertion position. |
update_column(column_id, updates) |
ColumnRecord |
Rename from {"title": ...}. |
move_column(column_id, index) |
ColumnRecord |
Reorder to an exact position. |
delete_column(column_id, *, delete_cards=False) |
ColumnRecord |
Empty-only unless cascade is explicit; no confirmation prompt. |
Each board mutation enforces its action setting and emits the corresponding event only when data changes.
Board editor and loading methods
| Member | Return | Behavior |
|---|---|---|
open_add_card_editor(column_id=None) |
None |
Open the generated drawer. It is a no-op when use_builtin_editor=False; otherwise, if no column exists and adding columns is allowed, prompt for one first. |
open_edit_card_editor(card_id) |
None |
Open the drawer or invoke the configured custom callback; invalid/unknown IDs are ignored. |
open_add_column_dialog() |
None |
Prompt for a title and create a UUID-backed column. |
is_loading |
bool |
Read-only property describing pending async delivery/manual presentation. |
load_error |
`Exception | None` |
set_loading(loading) |
None |
Toggle presentation; requires a real boolean. |
load_async(fetch_snapshot, *, on_success=None, on_error=None, clear_on_error=False) |
threading.Thread |
Start validated background loading. |
destroy() |
None |
Cancel scheduled delivery and editor motion, release menus/grabs/scroll bindings, and tear down widgets. Safe to call more than once. |
BoardModel
The Tk-free BoardModel(columns=(), cards=(), *, fields=None) is useful in
repositories, tests, command-line tools, and preprocessing layers. It supplies:
snapshot()
load(data=None, *, columns=None, cards=None)
get_card(card_id) / get_cards(column_id=None) / get_columns()
get_fields() / set_fields(fields)
add_card(card, *, index=None)
update_card(card_id, updates=None, **changes)
delete_card(card_id)
move_card(card_id, column_id, index=None)
reorder_card(card_id, index)
add_column(column, *, index=None)
update_column(column_id, updates=None, **changes)
delete_column(column_id, *, delete_cards=False)
move_column(column_id, index)
clear()
Unlike the widget's forgiving get_card(), BoardModel.get_card() raises
BoardModelError for an invalid or unknown ID. The model does not enforce
BoardConfig permissions or emit events. Its update_card() and
update_column() accept keyword changes in addition to an optional mapping;
model update_column() also accepts a title string directly.
Public records and helpers
| Name | Kind and purpose |
|---|---|
Column |
Frozen dataclass with id and title; from_definition() validates and trims a mapping. |
Card |
Frozen compatibility dataclass for the four default fields; tags are a tuple. It is not a container for arbitrary schema fields. |
ColumnRecord |
TypedDict output with id and title. |
CardRecord |
dict[str, Any] because schema and private values are dynamic. |
BoardSnapshot |
TypedDict with columns and cards lists. |
CardField |
Frozen, typed convenience definition with readable visible/editor/search defaults. |
Field |
Fluent builder for card input type, compact display, validation, formatting, and visibility. |
FieldDefinition |
TypedDict describing a generated field. |
FieldInput |
Accepted union of str, CardField, Field, and legacy field mappings. |
FieldType |
Literal union of supported field type strings. |
DEFAULT_FIELDS |
Tuple containing detached-compatible definitions for title, description, priority, and tags. |
ActionConfig, LayoutConfig, TextConfig, BoardConfig |
Frozen configuration dataclasses. |
DEFAULT_THEME |
Import-time dictionary of all theme defaults/tokens. |
merge_config(), merge_theme() |
Strict validation/merge helpers. |
normalize_row(), normalize_rows(), rows_from_cursor() |
Row-to-dictionary adapters. |
snapshot_from_rows(), snapshot_from_cursors() |
Schema-aware row/cursor adapters with optional card_keys and column_keys mappings. |
__version__ |
Installed CTkKanban version string. |
Card.from_definition() intentionally uses the default schema and returns the
fixed dataclass. Use a BoardModel(fields=...) when custom keys must remain in
the result.
Errors and lifecycle
BoardModelError subclasses ValueError and is raised for invalid records,
schemas, IDs, indices, relationships, updates, protected deletions, and board
actions disabled by configuration. Configuration helpers also use TypeError
for incorrect value kinds and ValueError for invalid names/ranges. Theme
helpers reject unknown token names with ValueError; CustomTkinter may report
invalid token values later while constructing a widget.
Editor validation errors are shown in the drawer and keep it open. Public
method errors are not swallowed. on_change and on_card_open callback
exceptions are logged so the Tk interaction can finish; async success/error
callbacks are application code and should handle their own failures.
All widget construction, mutations, and direct UI access should occur on the
Tk thread. load_async() is the provided exception: only its fetch callable
runs in a daemon worker, and that callable must stay independent of Tk.
Call destroy() during normal widget teardown; it invalidates outstanding
async delivery and cleans up window-level bindings and local drag state.
Migrating from 1.x
Version 2 is intentionally breaking. Replace mutation-specific callbacks with
on_change, and import from ctk_kanban rather than CTkKanBan. The focused
2.0 API initially removed dynamic fields; the current schema API restores that
capability without restoring the former persistence, filtering, sorting, and
large constructor-flag frameworks. Custom record keys are now preserved.
| 1.x concept | Current replacement |
|---|---|
from CTkKanBan import ... |
from ctk_kanban import ... |
| Dynamic/generated fields | The smaller fields schema documented above. |
| Inline and popup editing modes | One embedded explicit-save drawer, or on_card_open. |
| Mutation-specific callbacks | One on_change(event) callback with before/current snapshots. |
| Built-in persistence/data sources | Application repositories plus snapshots, row adapters, and load_async(). |
| Advanced filters/sorts | Schema-aware local search and manual order; transform source data in the host. |
| Many constructor booleans | BoardConfig plus a small compatibility override set. |
Migrate stored records by aliasing each card's owning value to column, keep
IDs stable and type-consistent, and add field definitions for values that need
UI, validation, search, or compact display. Leave integration-only keys
undefined in the schema if they only need to round-trip.
Development
python -m pip install -e ".[dev]"
python -m pytest -q
python -m ruff check .
python -m mypy ctk_kanban
python -m build
Release maintainers should also read docs/publishing.md.
The changelog records user-visible behavior in CHANGELOG.md.
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 ctkkanban-2.2.1.tar.gz.
File metadata
- Download URL: ctkkanban-2.2.1.tar.gz
- Upload date:
- Size: 142.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6fb847365b043b0539223a2640f311b20da58f64a4aba2c133f4aa9c1bd08c00
|
|
| MD5 |
87983a98c0dfd33c5bb02c4bf117adbb
|
|
| BLAKE2b-256 |
104b9018917be6255d5be1505c88e9c25ae914c0fc02403c15994679c3180614
|
Provenance
The following attestation bundles were made for ctkkanban-2.2.1.tar.gz:
Publisher:
publish.yml on Harry-g25/CTkKanBan
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
ctkkanban-2.2.1.tar.gz -
Subject digest:
6fb847365b043b0539223a2640f311b20da58f64a4aba2c133f4aa9c1bd08c00 - Sigstore transparency entry: 2580047175
- Sigstore integration time:
-
Permalink:
Harry-g25/CTkKanBan@f39a950ea826286429509e20724a088adc0eb486 -
Branch / Tag:
refs/tags/2.2.1 - Owner: https://github.com/Harry-g25
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@f39a950ea826286429509e20724a088adc0eb486 -
Trigger Event:
release
-
Statement type:
File details
Details for the file ctkkanban-2.2.1-py3-none-any.whl.
File metadata
- Download URL: ctkkanban-2.2.1-py3-none-any.whl
- Upload date:
- Size: 61.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f09741a035def7be119b405916c678686ce224f789e44e0744318456bc1f442a
|
|
| MD5 |
be0405edc3af5e4cdb8b6ff942cf8697
|
|
| BLAKE2b-256 |
e276eae69ac66bc86b54cfad6821ad637a5541c1f0e7575a48c13585097e48fc
|
Provenance
The following attestation bundles were made for ctkkanban-2.2.1-py3-none-any.whl:
Publisher:
publish.yml on Harry-g25/CTkKanBan
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
ctkkanban-2.2.1-py3-none-any.whl -
Subject digest:
f09741a035def7be119b405916c678686ce224f789e44e0744318456bc1f442a - Sigstore transparency entry: 2580047184
- Sigstore integration time:
-
Permalink:
Harry-g25/CTkKanBan@f39a950ea826286429509e20724a088adc0eb486 -
Branch / Tag:
refs/tags/2.2.1 - Owner: https://github.com/Harry-g25
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@f39a950ea826286429509e20724a088adc0eb486 -
Trigger Event:
release
-
Statement type: