Skip to main content

taut.tk

A small SolidJS-inspired runtime for Tkinter using reaktiv for fine-grained signals.

This is currently a prototype that I hope makes Tkinter more fun

from typing import Protocol

from taut import component
from taut import layout
from taut import tk
from taut.reactive import Accessor
from taut.reactive import Mutator
from taut.reactive import create_signal
from taut.runtime import create_root
from taut.control import For
from taut.control import Show


class CounterProps(Protocol):
    label: Accessor[str]
    count: Accessor[int]
    set_count: Mutator[int]


@component
def counter(props: CounterProps):
    todos, set_todos = create_signal(["wire props", "own effects", "dispose cleanly"])

    return layout.VStack(
        tk.Label(text=lambda: f"{props.label()}: {props.count()}"),
        tk.Button(text="Increment", on_click=lambda: props.set_count(lambda n: n + 1)),
        Show(
            lambda: props.count() % 2 == 0,
            lambda: tk.Label(text="Even"),
            fallback=lambda: tk.Label(text="Odd"),
        ),
        For(todos, lambda item: tk.Label(text=item), key=lambda item: item),
        layout.HStack(
            tk.Button(text="-", on_click=lambda: set_todos(lambda items: items[:-1])),
            gap=6,
        ),
        padding=12,
        gap=6,
    )

count, set_count = create_signal(0)
mount = create_root(
    lambda: counter(label="Solid TK", count=count, set_count=set_count),
    title="Solid TK",
)
mount.widget.mainloop()

Why Tk?

I keep reaching for it every time I want a small UI at work and I keep getting bogged down in abstractions I think are loose but turn out to be more tightly-coupled than I realize. Imperatively updating state is also a chore and makes for widgets with lots of clunky helpers, and I find widget vars unwieldy. My most recent attempt I threw reaktiv at the problem and was surprised at how streamlined it made my widgets. I felt it could be more. So here we are.

What's Here So Far

  • Functional components using @component decorator
  • Class Component with __init__()/setup() and render()
  • Props, where every attribute is an accessor
  • first-class component children through props.children
  • transparent Fragment(...) nodes for returning multiple children
  • widget namespaces: tk for classic Tk widgets and ttk for themed widgets
  • layout namespace: layout.VStack, layout.HStack, layout.Grid, layout.Item, layout.GridItem
  • StyleX-ish style objects agnostic to tk/ttk widgets with style.define(), style.merge(), and style.component().
  • some control flow: Show, For, Switch / Match, Index, Dynamic
  • context: create_context(), Provider, use_context()
  • stores: create_store() for immutable updates and create_mutable() for Solid-style mutable object state
  • resources: create_resource() with loading, error, state, mutate, refetch
  • lifecycle helpers: create_effect(), on_mount(), on_cleanup()
  • create_root() and explicit disposal through the returned Mount

Doc Topics

  • Context: providers, typed context helpers, and forwarded children.
  • Control: conditional rendering.
  • Reactive primitives: signals, memos, effects, and owner cleanup.
  • Resources: worker-thread loading, refreshes, mutation, and status.
  • Scheduling: queueing work
  • Stores: state management
  • Style: StyleX-ish style objects, merging, and component styling.
  • Widgets: Tkinter primitives

Examples

The runnable examples live in examples. Start with the examples README for a short path through them.

Strong Typing

To follow Solid's model of named args collected into reactive props, there is some brittle trickery.

In JS/TS land, there's tons of tooling around jsx/tsx. Python doesn't have that.
What it does have are .pyi files. We can define parameter transformations that type checkers can't inspect and then give them the publc facing API with a wink and a smile. The problem is generating those .pyi files.

The beginning of solving that problem is the @component decorator. In addition to transforming functions into rendered nodes, is also a syntax marker that can be inspected.

The next bit is stub-genning; basically a build step. Files in the working directory are scanned for component declarations and a corresponding .pyi file is generated, unrolling the typed prop object into a function signature.

This process can be manual with uv run taut.tk stubs ., or you can run a watcher to do it live with uv run taut.tk watch (it might be brittle though).

There are some caveats though, as this bit is honestly harder and more involved than the actual framework to get right. It imposes some limitations on import patterns.

When a component module imports sibling modules, prefer from . import <module> or a direct submodule import like import examples.todo.module. Relying on imports that use __init__.py, like from examples.todo import module, can route through generated package stubs instead of the source module. This is a limitation imposed by the current state of type checkers, and the only real work around that preserves usage of __init__.py is stubbing everything. Not really a feasible thing to develop or maintain right now.

Design Notes

create_signal() returns an accessor and a mutator:

from taut.reactive import create_signal
count, set_count = create_signal(0)
count()
set_count(lambda value: value + 1)

The accessor is still compatible with reaktiv signals, so widgets can bind to it directly. Writable widgets such as tk.Entry(value=..., on_input=...) receive the accessor and mutator separately.

Component.__new__ returns a renderable node to keep the class API simple

Counter(title="Solid TK")

Inside a component, self.props.title() reads a reaktiv signal. This is intentionally accessor-oriented.

Existing signals are preserved, while plain values and callbacks are wrapped as signal values. Tk widget props use one additional convention: callable non-event props are treated as reactive bindings, and event props such as on_click / command are passed through as callbacks.

tk.Label(text=f"{count()}")                  # snapshot now
tk.Label(text=count)                         # reactive signal value
tk.Label(text=lambda: f"{count()}")          # reactive derived expression

That means a component prop can be read inside a derived expression:

tk.Label(text=lambda: f"Hello {self.props.name()}")

or forwarded directly to a widget prop:

tk.Label(text=self.props.name)

Component children are also collected into props.children, so container-like components can feel like Solid components instead of manually passing a children= prop:

@component
def panel(props):
    return layout.VStack(
        tk.Label(text=props.title),
        props.children(),
        padding=8,
    )

panel(tk.Label(text="Nested"), title="Details")

Control-flow nodes and Fragment(...) are transparent to layout. They produce children; the parent widget or layout helper decides where those children go:

@component
def rows(props):
    return For(props.items, lambda item: tk.Label(text=item), key=lambda item: item)

layout.VStack(
    tk.Label(text="Before"),
    rows(items=todos),
    tk.Label(text="After"),
)

In that example, the repeated labels are laid out by VStack between Before and After; For and the component do not create wrapper frames.

Benchmarks

To estimate framework overhead without needing a display server, run the fake-Tk benchmark:

uv run python benchmarks/bench_overhead.py

It compares raw Tk-style widget creation with taut.tk mounting for static labels, reactive label props, and a component wrapper. The most useful line is the reported extra microseconds per widget, because native Tk/Tcl startup and platform display behavior are intentionally excluded.

Metadata

Release files for taut.tk 0.1.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for taut.tk 0.1.1
File Size Uploaded
taut_tk-0.1.1.tar.gz 34.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for taut.tk 0.1.1
File Interpreter ABI Platform
taut_tk-0.1.1-py3-none-any.whl Python 3 none any Details

Total release size: 77.7 kB

Release files / taut_tk-0.1.1.tar.gz

Download URL taut_tk-0.1.1.tar.gz
Size 34.6 kB
Tags Source
SHA-256 checksum
How to use checksums
ebf4fb896837c09b89839e081137a43ebf4e246a411561e7b2ec7bbf4f0e1557
BLAKE2b-256 checksum
How to use checksums
d409e5dea500cbdfa27f651d72aef813f4e8152dd9846e6ebdc0f59d2e595054
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 15, 2026.

Transparency log

Release files / taut_tk-0.1.1-py3-none-any.whl

Download URL taut_tk-0.1.1-py3-none-any.whl
Size 43.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
c9b0f2930e82edf2cf696495548857bdda4401cc45962e1d8e3f450614463d52
BLAKE2b-256 checksum
How to use checksums
3fa35e419d4edb44685535c721357d118428afd257543453c5e957afb24d11f9
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 15, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.1.1 This release

2 release files

0.1.0

2 release files

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