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
@componentdecorator - Class
Componentwith__init__()/setup()andrender() Props, where every attribute is an accessor- first-class component children through
props.children - transparent
Fragment(...)nodes for returning multiple children - widget namespaces:
tkfor classic Tk widgets andttkfor 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(), andstyle.component(). - some control flow:
Show,For,Switch/Match,Index,Dynamic - context:
create_context(),Provider,use_context() - stores:
create_store()for immutable updates andcreate_mutable()for Solid-style mutable object state - resources:
create_resource()withloading,error,state,mutate,refetch - lifecycle helpers:
create_effect(),on_mount(),on_cleanup() create_root()and explicit disposal through the returnedMount
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)
| File | Size | Uploaded | |
|---|---|---|---|
| taut_tk-0.1.1.tar.gz | 34.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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 logRelease 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