The shared framework and PySide6 backend support Python 3.9. Shared and Qt regression suites pass on Python 3.9.24 with PySide6 6.7.2 on Linux; embedded-host integration and other platforms need separate validation. Optional backends can require newer Python: the supported Flet 1.0.4 runtime requires Python 3.10+. See Python 3.9 compatibility for installation and the repeatable make check-python39 baseline check.
The Qt test runtime is pinned to PySide6==6.7.2 in tests/requirements-python39.txt; make check-python39 rejects a different version.
Installation extras: QPUIQ[pyside6], QPUIQ[wx], QPUIQ[flet], and QPUIQ[textual]. For Tkinter, install plain QPUIQ with native Python/OS Tk support; QPUIQ[tkinter] adds no pip dependency. See installation.
Backend alignment status and pinned PySide6 baselines: wx, flet, tkinter, and Textual. Textual has native headless Views/Tabs checks; broader alignment and terminal validation remain pending. wx, Qt and Tk have native regression coverage; Flet has serialized Session checks and live web Table editing/scrolling checks. Recorded env/ runtimes are PySide6 6.12.0, Tk 8.6, Flet 1.0.4 and Textual 8.2.8. Progress documents list the remaining checks.
Textual Text applies foreground/background colors and restores theme colors when those styles are removed. The cookbook code viewer uses its fixed dark palette in both terminal themes.
See the separate Web backend reference for installation, startup, authentication, deployment, browser widgets and transport limits. The experimental Web backend is not yet included in the matrix below. See installation for backend/development extras.
wx MenuBar contains Menu declarations; Menu contains nested Menu or MenuAction. .trigger(callback, *args, **kwargs) calls back without an event argument. Labels and callbacks update during reconciliation. wx dialog helpers accept dir or directory, Qt/wx filter strings, and an optional SaveFile default. wx cancellation returns None; PySide6 OpenDirectory currently returns an empty string.
Classes are the recommended API. Import Application and PUIView from the selected backend. Implement content() to declare the UI; initialize local state in setup(), which runs once when the view is first built. Constructors can accept explicit parameters and must call super().__init__().
class Example(Application):
def content(self):
with Window(title="Example"):
Label("Hello")
Example().run()
On wx, import PUIView from PUI.wx to schedule reactive redraws on the wx GUI queue. Redraw bursts are coalesced; removed views ignore queued work. Native widgets are reused during reconciliation and destroyed with their owner. Removing or replacing a virtual subview destroys its native roots, including controls inside wx sizer layouts. Keyed moves preserve those controls.
class Counter(PUIView):
def setup(self):
self.state = State()
self.state.count = 0
def content(self):
with HBox():
Label(str(self.state.count))
Button("+").click(self.increment)
def increment(self, event):
self.state.count += 1
Each decorator emits DeprecationWarning at its use site when it is applied; creating instances and redrawing existing views do not repeat that warning. Use Python's -Wd option to show deprecation warnings during migration.
@PUI and @PUIApp are deprecated but remain available for existing code. Replace a decorated function with a PUIView or Application subclass and move its body into content(self). Move parameters into an explicit constructor, initialize persistent local state in setup(), and use member event handlers. For example, @PUI def Greeting(text) becomes:
class Greeting(PUIView):
def __init__(self, text):
super().__init__()
self.text = text
def content(self):
Label(self.text)
Existing decorator argument forwarding, backend scheduling, and functools.wraps metadata remain supported during deprecation. inspect.signature() displays the builder signature and inspect.unwrap() returns the builder for source inspection. Calling the wrapper still creates a view/application; a builder's return annotation does not describe the wrapper's return value.
def button_cb(event, data):
print(data)
Button("button").click(button_cb, 1)
will invoke
button_cb(event, 1)
when the event is emitted.
The common target is approximate cross-axis centering: HBox vertically centers compact children, and VBox horizontally centers them. Expandable children may still fill their available space. This positions widget bounds, not text inside a Label or input; fonts, native dimensions and rounding differ.
| Backend | HBox | VBox | Limits / verification |
|---|---|---|---|
| Web | Per-child vertical center | Per-child horizontal center | See Web layout and sizing. |
| wx | Per-child vertical center | Per-child horizontal center | Native sizer flags preserve wx.EXPAND for growing children. Explicit dimensions remain minimum sizes; Linux GTK resize/redraw geometry checked. |
| Textual | Child group vertically centered | Child group horizontally centered | Native align centers the group's bounding rectangle. Different sizes or a full-size child can leave smaller siblings off-center; headless resize/redraw geometry checked. |
| PySide6 | Fixed-height controls center; others can fill height | Controls normally fill width; width-constrained controls remain left-aligned | Not fully aligned. Qt 6.7.2/6.12 geometry checked; native size policies determine expansion. |
| Flet | Native Row CENTER | Native Column START (left) | VBox still needs alignment. Flet 1.0.4 Session configuration checked; client geometry unverified. |
| tkinter | Entry centered; most controls fill row height | Most controls fill column width | Not fully aligned. Grid sticky defaults to nsew, with ew for TextField/MaskedField; Linux Tk geometry checked. |
See the backend progress documents for outstanding differences. PySide6's remaining VBox alignment also needs a choice of which native size policies should retain stretching before changing shared layout behavior.
Web-specific sizing priorities and browser layout rules are in Web layout and sizing.
PySide6's named .qt options are keyword-only. Omitting an option or passing None preserves its previously stored value on that declaration; pass StyleSheet={} to clear stylesheet overrides. Extra keywords remain accepted.
StateList.sort(*args, key=None, reverse=False, **kwargs) exposes the standard list sort options as keyword-only parameters. Defaults and any extra arguments are forwarded to the backing collection; ordinary lists reject extra arguments.
Every backend accepts keyword-only Application(title=''). The cookbook sets title="PUI Cookbook". PySide6/wx/Flet retain Application(icon=None, *, title=''); Textual uses Application(*, title=''), and Web/Tkinter retain positional forwarding with Application(*args, title=''). This is startup configuration, not a State binding.
Web and desktop Window(title=None) inherits a nonempty Application title. An explicit Window title, including an empty string, wins. Omitting Application.title preserves existing backend defaults.
Textual uses the existing Rich dependency's public Console.set_window_title on the launch output stream, strips terminal control characters and skips headless mode. Visibility depends on terminal support/settings; the prior title is not restored on exit.
Tkinter configures its theme with .tkinter(theme=None).
Closing the last Tkinter Window, removing it without a replacement, or calling Application.quit() ends the main loop and destroys the hidden Tk root, dialogs, timers and pending redraws. With multiple windows, closing one hides it while the others remain open; redraws retain its controls without reopening it. Replacing a window during reconciliation does not quit the application.
wx reactive views use the native GUI queue; import PUI and PUIView from PUI.wx. Redraw bursts are coalesced and removed views ignore queued work. Flet uses its session loop. Tkinter queues redraws and quit outside geometry idle flushes to prevent reconciliation reentry. Application.quit() is also queued on wx. TimelineView timers follow reconciled nodes and stop after removal.
Removing or replacing a virtual subview destroys its native roots, including controls inside wx sizer layouts. Keyed moves preserve those controls.
Top level element of an application
Application().qt(Style="fusion")
maximize/fullscreen constructor flags are initial-only on wx, PySide6, tkinter, and Flet. Use .maximize(enabled=True) or .fullscreen(enabled=True) for live changes; False clears the state. Later declarations preserve user window state. wx supports application icons, keypress events, and close/quit accelerators.
flet and textual backends only support single window
Window([title=str][,size=(w,h)][,maximize=bool][,fullscreen=bool])
PySide6 Modal follows the initial-only window-state contract and exposes imperative state methods. wx modal strategy remains pending.
Modal(model[,offValue=None][,title=str][,size=(w,h)][,maximize=bool][,fullscreen=bool])
Horizontal linear layout container. Children run left to right; flexible space is distributed horizontally. The default alignment target is vertical middle for compact children, while vertically expandable children can fill the height. This aligns widget bounds, not the text inside them.
Web and wx center non-expanding children individually. Textual centers the child group, so different heights can leave smaller children off-center. PySide6 and Flet use native vertical centering; growable Qt controls can fill the height. Tkinter centers TextField/MaskedField vertically, while most other controls stretch. See backend alignment details and Web sizing rules.
wx and tkinter apply four-sided padding in top/right/bottom/left order. Weights reset during reconciliation. wx width/height are minimum-size hints; Tk text dimensions retain native character/line units. Generic wx styles and Tk native/theme styles restore when removed.
Example
HBox()
Vertical linear layout container. Children run top to bottom; flexible space is distributed vertically. The default alignment target is horizontal center for compact children, while horizontally expandable children can fill the width. This aligns widget bounds, not the text inside them.
Web and wx center non-expanding children individually. Textual centers the child group, so different widths can leave smaller children off-center. Other backends still differ: Flet aligns left; PySide6 normally fills the width but leaves width-constrained controls at the left; Tkinter normally fills the width. See backend alignment details and Web sizing rules.
Example
VBox()
Tkinter uses native grid positions/spans, padding, and occupied-axis resize weights. Divider is ttk.Separator. Tk dialog helpers use filedialog/messagebox/simpledialog, accept Qt/wx filters and dir/directory aliases, and return None on cancellation.
with Grid():
Text("A").grid(row=0, column=0)
Text("B").grid(row=1, column=0, rowspan=2)
Tkinter exposes its inherited Spacer(layout=None, side=None) parameters; both accept positional or keyword arguments. Its default layout weight is 1.
wx and tkinter apply four-sided padding in top/right/bottom/left order. Weights reset during reconciliation. wx width/height are minimum-size hints; Tk text dimensions retain native character/line units. Generic wx styles and Tk native/theme styles restore when removed.
Example
with HBox():
Text("Left")
Spacer()
Text("Right")
with VBox():
Text("Top")
Divider()
Text("Bottom")
wx scrollX/scrollY use logical-pixel start offsets for positive values and end offsets for negative values. Scroll.END is negative zero. wx follows content growth at the end and preserves manual positions. Axis options: None (as needed), True (always), False (disabled). Tk Scroll source owns its outer container and filters wheel events locally; native resize/end-following checks are pending. Flet uses native async scroll_to and END auto_scroll; live client checks remain pending.
with Scroll().layout(weight=1).scrollY(Scroll.END):
with VBox():
for i in range(100):
Label(f"Row {i+1}")
Button(text, enabled=True).click(callback, *cb_args, **cb_kwargs)
enabled is keyword-only and defaults to True on all backends. False disables native user interaction; later declarations can re-enable the retained control. For reactive state, use Button("Save", enabled=state.can_save). Register handlers with .click(); Textual's unused constructor callback argument has been removed.
### Callbacks * .clickwx selectable=True uses a read-only TextCtrl. Changing selectability remounts the native control. Flet preserves native text selection: ordinary labels use Container.on_click and selectable labels use Text.on_tap. Both refresh or remove callbacks on redraw and commit focused editors before calling them. Native event tests and live web-client clicks pass.
Label(text).click(callback, *cb_args, **cb_kwargs)
### Callbacks
* .click
Text(text)
Flet's plain-text fallback accepts Html(text) or Html(text=...) and reports pui_supported=False. Flet MarkDown(text) accepts the same argument forms; native options belong in .flet(**kwargs).
wx uses native HtmlWindow and its supported HTML subset; browser CSS is not supported. wx rich Markdown remains pending a dependency/renderer decision.
Html(html)
MarkDown(md)
The ComboBox cookbook includes read-only/editable controls and a destination autocomplete with separate State, explicit editable=True, and distinct labels/airport-code values. Suggestion filtering depends on the native backend; see Web selection controls for browser suggestions and commit timing.
ComboBox(editable=False, index_model=None, text_model=None, *, enabled=True) accepts a keyword-only enabled flag on PySide6, wx, Flet, tkinter and Web. Disabling prevents user selection/typing while preserving values and allowing programmatic model updates. Re-enabling restores the editable mode; omitting enabled on a later declaration restores True. Textual ComboBox remains unsupported. Flet's explicit .flet(disabled=...) option overrides enabled for Button and ComboBox; removing it restores the current declaration's enabled value.
PySide6/wx/flet/tkinter share item labels/values and free text. Typing clears index_model to -1 and survives redraws with both models bound. Unbound selections preserve keyed identity. Tk now uses native ttk.Combobox.
wx safely handles empty/invalid selections and refreshes item labels. Changing native editability remounts the control. Flet uses native Dropdown and Tkinter uses ttk.Combobox.
with ComboBox(editable=True, index_model=state("index"), text_model=state("text")):
ComboBoxItem("Item 1")
ComboBoxItem("Item 2")
ComboBoxItem("Item 3")
with ComboBox(editable=False, text_model=state("text")):
ComboBoxItem("Item 1", "Value 1")
ComboBoxItem("Item 2", "Value 2")
ComboBoxItem("Item 3", "Value 3")
Supported controls commit focused edits before click callbacks. Flet retains native focus where its public API has no blur operation; it does not invoke undocumented client methods.
wx, flet, and tkinter silently apply programmatic updates, preserve active edit buffers, and commit once on Enter/submit or blur. input/change events carry .value. Native wx/Tk and Flet session tests pass; Flet editor commits also pass a live web-client smoke check.
TextField(model, edit_model=None, *, placeholder=None)
placeholder supplies native hint text while the field is empty on PySide6, wx, Flet, and Textual. It updates on redraw without changing the model or active edit buffer. None, an empty string, or omission on the next declaration clears it. Visibility while focused follows the native backend. Tkinter accepts the argument without displaying it because the current Tk 8.6 Entry has no native placeholder; emulation is deferred.
TextField(state("name"), placeholder="Enter a name")
Flet retains its existing third positional label and native keyword forwarding. An explicit hint_text keyword or .flet(hint_text=...) overrides placeholder; removing it restores the current placeholder. Textual suppresses programmatic Input.Changed messages with the native API so the first keystroke in an initially empty field reaches .input() normally.
Single-line password editor available on PySide6, wx, Flet, tkinter, Textual and Web.
MaskedField(model, edit_model=None, *, placeholder=None)
MaskedField(state("password"), placeholder="Password")
Inherits TextField's binding/commit behavior, input/change callbacks, style and layout. Optional edit_model receives live drafts; model and event.value contain the original text. Masking affects display only. Native password controls supply mask glyphs and clipboard behavior. See Web text inputs for browser editor handling.
Flet retains the third positional label and native keyword forwarding. Tkinter accepts placeholder without showing a hint, as with TextField.
Example: plain and buffered bindings, lengths and clearing without displaying or logging passwordswx and PySide6 convert fractional progress/ranges to native integer hundredths.
ProgressBar(progress `0-1`)
Checkbox(label, model)
RadioButton(label, value, model)
See Web Canvas for browser rendering, command diffs, pointer/wheel events, text semantics, limits and performance measurements.
The HeavyCanvas cookbook uses a static PUIView class and standard-library seeded random data. Its English chart shows OHLC candles, 20-session Bollinger bands (two population standard deviations), volume, smoothed stochastic K/D (9-session range, smoothing 3) and Wilder RSI 14. It defaults to 500 weekday sessions; 1,650 and 6,000 presets produce roughly 15,000 and 54,000 individual Painter commands per frame. Regenerate advances the seed. Vertical wheel input zooms around the pointer: up keeps 80% of visible sessions, down expands by 1.25x, rounded and bounded to 20 sessions through the full dataset. Only direction is used, with no pixel-to-angle conversion. Price and volume axes autoscale to visible rows. Reset zoom, changing the preset or Regenerate restores the full range. Visible session counts, zoom and draw-call estimates are displayed. Generated data and indicators are cached through zoom/hover; visible row ranges and axis limits are reused between zoom changes. The painter redraws all panels, a crosshair and a tooltip, producing large command replacements on zoom and small tooltip deltas on hover. The scrollable chart requests a 720-logical-pixel height and uses its measured drawing width. Tkinter still uses requested dimensions and lacks native Canvas hover/wheel events, so its controls update a static chart without crosshair/tooltips or wheel zoom.
Flet canvases with .layout(weight=1) fill their available native drawing space. Paint callbacks wait until both dimensions are positive and finite, so the first callback can fit a drawing. Identical resize notifications do not repaint; model redraws still invoke the current painter.
Flet 1.x uses native Text/Path/Rect/Oval, including cubic paths and rotated text. Image(path) sends native image bytes; ComboBox uses Dropdown with distinct item values. Canvas image crop/resources, pointer events, and text height clipping remain pending.
wx includes ImageResource.load/crop/scale, loadImage/drawImage source rectangles and opacity, cubic paths, enter/leave, and wheel modifiers. Source image opacity is preserved; native wheel pixel deltas are zero when unavailable. Queued paints after removal are ignored. Tk Canvas source adds native polygon/rectangle/ellipse and text size/color/rotation/anchors; cubic paths, images, and text height clipping are incomplete.
Example
def painter(canvas):
canvas.drawText(x, y, text, w=None, h=None, size=12, color=None, rotate=0, anchor=Anchor.LEFT_TOP)
canvas.drawLine(x1, y1, x2, y2, color=0xFF0000, width=1)
canvas.drawPolyline([x1, y2, ..., xn, yn], color=0xFF0000, width=1)
canvas.drawCubicBezier(start, control1, control2, end, color=0xFF0000, width=1)
canvas.drawRect(x1, y1, x2, y2, fill=0xFF0000, stroke=0x00FF00, width=1)
canvas.drawShapely(shape, fill=0xFF0000, stroke=0x00FF00, width=1)
Canvas(painter)
drawCubicBezier() is available on PySide6, wx, and Flet.
start, control1, control2, and
end are (x, y) coordinate pairs defining a cubic
Bézier curve.
Flet dispatches all seven Canvas callbacks through native GestureDetector events. Coordinates are local logical pixels. Left/right press, drag and release continue outside the Canvas; middle-button taps use the tertiary tap recognizer. Hover has button=MouseButton.NONE, while recognized drags report their button. Registration and callbacks update on redraw; removed canvases ignore queued events, and drawing-only canvases omit the gesture wrapper.
Flet follows native gesture timing and thresholds. Double-click is primary-button only and fires after recognition at the second press position. Cancelled or consumed presses report mouseup with cancelled=True at the last reported position; normal releases have cancelled=False. Clear drag state on cancellation without treating it as a completed edit. Middle-button dragging, extra buttons and simultaneous-button tracking are unavailable.
Wheel x_delta/y_delta contain pixel deltas, positive left/up to match PySide6. Angular h_delta/v_delta stay 0; pixels are never converted to angles. modifiers=None means Flet did not report modifiers. Consumers using only v_delta need an application-level pixel-delta path.
data = [(0,0), (1,3), (2,2)]
def plot(figure, data):
figure.clear()
sp = figure.add_subplot(111)
sp.axes.plot([d[0] for d in data], [d[1] for d in data])
MatplotlibCanvas(plot, data)
wx composes native SplitterWindow controls for any number of panes. Extra children nest in the right pane, A | (B | C), or the bottom pane with vertical=True. Moving an earlier sash resizes the remaining group. Declare nested Splitter blocks explicitly for another grouping; there is no automatic nesting-direction option.
Widgets, layouts, and nested splitters can be panes. Keyed moves retain controls and state; ordinary redraws retain sash positions. Changes to pane order/count or orientation redistribute space equally, and later window resizes share size changes equally among panes. A one-pixel minimum prevents native dragging or double-clicking from unsplitting declared content. Empty and single-pane splitters have no sash.
with Splitter():
Label("Left")
Label("Right")
See Web Views and Tabs for browser page retention and keyboard behavior.
Views(model=None) displays one child view without navigation controls. Each direct child needs a unique non-empty string .id(). Widgets, layouts, and PUIView instances are supported; multiple root controls in one view are grouped vertically. Declared views stay alive while hidden; removing a declaration destroys that view.
The optional model is a writable binding containing the selected ID. Without one, Views owns an internal model initialized to None and preserved across redraws. .select(key) updates the model and schedules a redraw; .select(None) clears the selection. None or an undeclared ID shows an empty container without rewriting the model. Keyed reordering retains views and selection; older handles follow reconciliation, and removed containers ignore selection requests.
with Views() as views:
EditorView().id("editor")
SettingsView().id("settings")
# In an event handler:
views.select("editor")
Use Views(model=state("current_view")) for external state. Views supports PySide6 through QStackedWidget, tkinter through retained ttk.Frame pages, Textual through ContentSwitcher, and wx through Simplebook; remaining native validation is tracked in the backend progress documents.
The cookbook Views page demonstrates both .select() with an internal model and selection by assigning a State field, with a stateful editor in each container.
State bindings preserve None, so .select(None) also clears an external model.
Flet Views is deferred: its current native controls do not provide both programmatic-only switching and guaranteed retention of inactive native pages.
See Web Views and Tabs for browser headers, keyboard navigation and retained pages.
Tabs(tabposition=Tabs.NORTH, model=None) uses an internal selection model by default. Pass model=optional_state("selection") for an external binding. Native tab clicks, .select(key), and bound-state assignments share the selection; older handles follow redraws. All five backends support this model.
Selection uses each Tab's explicit .id(), otherwise its text. The first match wins duplicate IDs/text, including clicks on later duplicates. Explicit IDs survive label changes; keyed moves retain pages and selection. The internal model initially selects the first tab. None or unknown keys display the first available tab without rewriting the requested value; an empty Tabs has no page. .select(None) returns to the first tab. Restoring a removed selected key restores its requested selection.
The cookbook demonstrates both Tabs() with .select() buttons and Tabs(model=...) controlled by optional_state, using both explicit IDs and text fallback. Textual uses native Tabs and ContentSwitcher with NORTH/SOUTH placement; EAST/WEST raises NotImplementedError.
PySide6 applies QTabWidget placement; Tk uses native ttk tabposition style. Flet supports NORTH/SOUTH. Theme/platform appearance can differ.
Flet 1.x uses Tabs/TabBar/TabBarView, supports NORTH/SOUTH, and preserves keyed page selection. Native Container shells apply generic layout parameters. Install with QPUIQ[flet].
wx uses native Notebook pages with keyed moves and reactive labels. Placement changes remount the notebook. wx Image(path) uses StaticBitmap with mtime reload and aspect-preserving explicit dimensions; weighted fill/stretch remains pending.
Example
with Tabs():
with Tab("Tab 1"):
Label("Tab Content 1")
with Tab("Tab 2"):
Label("Tab Content 2")
Stack overlays children, with later declarations above earlier ones. See Web Stack for browser layout and hit testing.
TimelineView(ttl_sec) requests reactive redraws periodically. Timers stop after removal. See Web TimelineView for worker scheduling.
See Web Table for inline editors, commit timing, draft retention, sizing and browser limitations.
TableNode(data, *, display=None, choices=None, prefix='', suffix='', placeholder=None) keeps the raw editable value separate from presentation. display replaces only the displayed value; prefix and suffix surround it in both the cell and its editor and are never written back. Decorations are literal, with no automatic punctuation or spacing: TableNode(100, prefix="Price: $", suffix=" USD") displays Price: $100 USD while editing only 100. placeholder is passed only to an editable cell's native text/choice editor and never affects display or committed data. Boolean editors have no placeholder. Tkinter's Tk 8.6 Entry, wx choice editors, and Qt numeric editors accept the shared API but have no native placeholder.
Textual supports TableNode rows and BaseTableAdapter with native DataTable, literal text, row/column labels, a cell cursor, keyboard navigation and scrolling. Enter opens a Save/Cancel ModalScreen: choices use Select, booleans use Checkbox, and str/int/float/None use Input. Values retain their types, duplicate labels stay distinct, and None is a valid choice separate from no selection. Escape cancels the dialog (or first closes an expanded Select). Draft validation/cancellation follows the Tkinter rules below.
Keyed Textual rows retain the cursor and unchanged drafts. Updates reuse DataTable; row-order/schema changes reset its data and restore the cursor by row key and scroll offsets. autofit=True follows native content sizing; False retains initial column widths in terminal cells. Callbacks follow native activation: clicking a different cell moves the cursor; clicking the highlighted cell or pressing Enter invokes .click(). A double-click activation invokes .dblclick() and opens an editable cell. Navigation and programmatic updates do not invoke callbacks. Cell widgets, drag/drop or automatic header sorting, native column resizing and Qt-style multi-selection are not implemented. See Textual progress for remaining terminal/platform validation.
Tkinter supports TableNode rows and BaseTableAdapter with ttk.Treeview, headers, native row selection, and horizontal/vertical scrollbars. Clicks dispatch cell callbacks. Double-click an editable cell for a Save/Cancel dialog; Enter edits the focused row's last clicked column (initially the first). Choices use a readonly Combobox in the dialog, preserving typed values and duplicate labels. Other editors support str/int/float/bool and None (as text). Invalid numbers or missing choices do not write back. Bindings or .set() callbacks enable editing; choices alone do not. Display stays separate from raw data; ragged cells are inert blanks.
The Tk editing dialog takes modal input and focus once visible; it can be canceled safely if the cell/table disappears before it opens. Keyed Tk rows retain selection and drafts through reordering. Removing a cell/table, changing its value/choices/binding, or making it read-only cancels the draft. Adapter drafts cancel on reconciliation because adapters lack stable row keys. autofit=True measures displayed text; False retains native/user widths. Native selection is row-based. Cell widgets, drag/drop or automatic header sorting, and per-cell styles are not implemented. See Tk progress for pending platform and keyboard checks.
wx uses wx.grid.Grid with TableNode rows or BaseTableAdapter data. Text/numbers edit inline; choices and booleans use native dropdowns. Raw types and duplicate labels are preserved; display text stays separate. Bindings or .set() callbacks enable declarative editing, while missing cells and arbitrary objects without choices are read-only. Invalid numeric edits leave the model unchanged. Headers, selection/scrolling, click/double-click callbacks, and both autofit modes are supported. Redraw/removal cancels unfinished edits; autofit=False preserves native/user column widths. See wx progress for remaining validation and large-data work.
PySide6 refreshes reused adapters with a native model reset, including row/column count changes. A reset may clear selection or an active editor.
Flet supports TableNode rows and BaseTableAdapter using its built-in DataTable. Editable choices cells use Dropdowns; other editable cells open a Save/Cancel dialog. Dialog values support strings, integers, floats, booleans, and None (edited as text). Raw choice values keep their types even with duplicate labels. Display text is separate from raw editing data. Choices alone do not enable editing; missing cells are inert blanks. Choice cells give gestures to the Dropdown, so cell click/double-click callbacks apply to ordinary cells. An explicit choice display value appears as the Dropdown label.
Keyed TableNode rows/cells retain drafts on reorder. Removing a cell/table, changing its raw value, or disabling editing cancels its draft. Adapter dialogs cancel on table reconciliation because adapters have no stable row keys. Native horizontal/vertical scrolling is provided; headers scroll with the data. Only content sizing (autofit=True) is supported; autofit=False raises NotImplementedError. Qt cell selection, additional editor types, and paging remain outside this implementation. See Flet progress.
with Table():
with TableNode():
TableNode(state("price"), prefix="Price: $", suffix=" USD",
placeholder="Enter price")
TableNode("A2")
with TableNode():
TableNode("B1")
TableNode("B2")
Example with adapter
Table(adapter)
wx uses DataViewCtrl with TreeNode declarations or BaseTreeAdapter data. The content column autosizes after model updates. Keyed nodes and stable adapter objects retain selection/expansion across redraws, reordering, relabeling, and adapter moves. Node click/double-click callbacks receive None; expand/collapse callbacks have no event argument. Adapter callbacks receive the node object. expandAll(), collapseAll(), and expandable(enabled) work before mounting and on a live Tree; disabling user expansion still permits imperative changes. Drag/drop awaits the shared contract.
with Tree():
with TreeNode("Root"):
with TreeNode("Sub 1"):
TreeNode("Sub 1-1")
TreeNode("Sub 1-2")
TreeNode("Sub 2")
with TreeNode("Sub 3"):
TreeNode("Sub 3-1")
Example with adapter
Tree(adapter)