Maintainer update: Open to opportunities. koxudaxi.dev
Parser-first HTML and T-HTML backends for PEP 750 template strings.
Documentation: tstring-html.koxudaxi.dev
Python 3.14 introduces t-strings. They look like f-strings but give you structured access to the interpolation values instead of concatenating them into a string. This project uses that structure to parse the template as HTML first, then insert escaped values into the right slots. The result is always valid HTML, and XSS is not possible.
| Package | What it does |
|---|---|
| html-tstring | Plain HTML rendering with auto-escaping |
| thtml-tstring | Adds JSX-style component tags on top of html-tstring |
tstring-html-bindings (the native extension) is pulled in automatically.
Requires Python 3.14+.
pip install html-tstring
pip install thtml-tstringOr with uv:
uv add html-tstring
uv add thtml-tstringPre-built wheels are available for Linux x86_64, macOS Apple Silicon, and
Windows x86_64. On other platforms you need Rust 1.94.0+ to build
tstring-html-bindings from source.
Interpolated values are HTML-escaped automatically:
from html_tstring import render_html
name = "<script>alert('xss')</script>"
page = render_html(t"<div class='greeting'>Hello, {name}!</div>")
# <div class='greeting'>Hello, <script>alert('xss')</script>!</div>The class attribute accepts strings, lists, and conditional dicts,
similar to clsx in the JS ecosystem:
from html_tstring import render_html
classes = ["btn", {"btn-primary": True, "btn-disabled": False}]
page = render_html(t'<button class="{classes}">Click</button>')
# <button class="btn btn-primary">Click</button>Pass a dict as a bare interpolation to spread it across the tag:
from html_tstring import render_html
attrs = {"data-id": "42", "hidden": False, "class": "extra"}
page = render_html(t'<div class="base" {attrs}>content</div>')
# <div class="base extra" data-id="42">content</div>T-HTML is a small DSL on top of t-strings. It adds one rule: a tag whose
name starts with an uppercase letter (e.g. <Card>) is treated as a
component call. The tag name is looked up as a Python callable, attributes
become keyword arguments, and nested content is normalized and passed as children.
There is no virtual DOM, no state management, no build step. It is just a
way to write reusable HTML fragments as functions and compose them with
familiar <Tag> syntax inside t-strings.
from string.templatelib import Template
from thtml_tstring import component, thtml
@component
def Card(*, children: str, title: str) -> Template:
return t"""
<div class="card">
<div class="card-header"><h3>{title}</h3></div>
<div class="card-body">{children}</div>
</div>
"""
@component
def Badge(*, children: str, tone: str = "info") -> Template:
return t'<span class="badge badge-{tone}">{children}</span>'
@component
def Button(*, children: str, **props: object) -> Template:
return t'<button {props}>{children}</button>'
# Compose them like JSX
user = "Alice"
status = "active"
result = thtml(t"""
<Card title='User Profile'>
<p>Name: {user}</p>
<p>Status: <Badge tone='success'>{status}</Badge></p>
<Button type='submit'>Save</Button>
</Card>
""")
html = result.render()The @component decorator wraps Template return values into a
Renderable automatically. You can also create a Renderable explicitly
with thtml() when you need to control scope or backend:
from thtml_tstring import Renderable
@component
def Badge(*, children: str, tone: str = "info") -> Renderable:
# explicit wrap, equivalent to the auto-wrap above
return thtml(t'<span class="badge badge-{tone}">{children}</span>')RawHtml still exists for injecting external trusted HTML strings, but
it is no longer needed for component composition.
Components are resolved from the caller's scope by default. For tests or framework integration you can pass the scope explicitly:
thtml(
t"<Button>Save</Button>",
globals={"Button": my_button_component},
locals={},
)t-linter is a linter, formatter, and
LSP server for t-strings. It uses the same Rust backends as this project for
check and format.
pip install t-linterCheck templates for errors:
t-linter check src/Format HTML / T-HTML template literals:
t-linter format src/Start the LSP server for real-time editor diagnostics:
t-linter lspA VSCode extension is also available.
Full docs: tstring-html.koxudaxi.dev
- Installation
- Quick Start
- HTML Usage
- T-HTML Components
- Editor Integration (t-linter)
- API Reference
- Spec Conformance Status
- Architecture
The repository includes typed examples that show how these APIs look in
real code, including spread attrs, Renderable composition, @component
auto-wrap, and explicit thtml(...) usage:
This repository makes repo-local v1 conformance claims rather than claiming full coverage of the external HTML ecosystem.
- HTML
defaultprofile: 34 manifest cases - T-HTML
defaultprofile: 47 manifest cases
The current matrix covers parser/backend seam behavior, Renderable
composition, scope capture, formatter raw_source fidelity, semantic spans,
and runtime boundaries. See the live status page:
uv sync --group dev
cargo test --manifest-path rust/Cargo.toml --workspace --tests
uv run pytest -q
uv run coverage run -m pytest -q && uv run coverage report
uv run ruff check .MIT