Tdom
A 🤘 rockin' t-string HTML templating system for Python 3.14.
Install / Use
/learn @t-strings/TdomREADME
tdom
A 🤘 rockin' t-string HTML templating system for Python 3.14.
NOTE WELL
This is pre-alpha software. It is still under heavy development and the API may change without warning. We would love community feedback to help shape the direction of this project!
Installation
You'll need Python 3.14, which was released in October 2025.
Then, just run:
pip install tdom
If you have Astral's uv you can easily try tdom in an isolated Python 3.14 environment:
uv run --with tdom --python 3.14 python
Usage
tdom leverages Python 3.14's
new t-strings feature to provide a
powerful HTML templating system that feels familiar if you've used JSX, Jinja2,
or Django templates.
T-strings work just like f-strings but use a t prefix and
create Template objects
instead of strings.
Once you have a Template, you can call this package's html() function to
convert it into a tree of Node objects that represent your HTML structure.
From there, you can render it to a string, manipulate it programmatically, or
compose it with other templates for maximum flexibility.
Getting Started
Import the html function and start creating templates:
from tdom import html
greeting = html(t"<h1>Hello, World!</h1>")
print(type(greeting)) # <class 'tdom.nodes.Element'>
print(greeting) # <h1>Hello, World!</h1>
Variable Interpolation
Just like f-strings, you can interpolate (substitute) variables directly into your templates:
name = "Alice"
age = 30
user_info = html(t"<p>Hello, {name}! You are {age} years old.</p>")
print(user_info) # <p>Hello, Alice! You are 30 years old.</p>
The html() function ensures that interpolated values are automatically escaped
to prevent XSS attacks:
user_name = "<script>alert('owned')</script>"
safe_output = html(t"<p>Hello, {user_name}!</p>")
print(safe_output) # <p>Hello, <script>alert('owned')</script>!</p>
Attribute Substitution
The html() function provides a number of convenient ways to define HTML
attributes.
Direct Attribute Values
You can place values directly in attribute positions:
url = "https://example.com"
link = html(t'<a href="{url}">Visit our site</a>')
# <a href="https://example.com">Visit our site</a>
You don't have to wrap your attribute values in quotes:
element_id = "my-button"
button = html(t"<button id={element_id}>Click me</button>")
# <button id="my-button">Click me</button>
Multiple substitutions in a single attribute are supported too:
first = "Alice"
last = "Smith"
button = html(t'<button data-name="{first} {last}">Click me</button>')
# <button data-name="Alice Smith">Click me</button>
Boolean attributes are supported too. Just use a boolean value in the attribute position:
form_button = html(t"<button disabled={True} hidden={False}>Submit</button>")
# <button disabled>Submit</button>
The class Attribute
The class attribute has special handling to make it easy to combine multiple
classes from different sources. The simplest way is to provide a list of class
names:
classes = ["btn", "btn-primary", "active"]
button = html(t'<button class="{classes}">Click me</button>')
# <button class="btn btn-primary active">Click me</button>
The class attribute can also be a dictionary to toggle classes on or off:
classes = {"active": True, "btn-primary": True}
button = html(t'<button class={classes}>Click me</button>')
# <button class="btn btn-primary active">Click me</button>
The class attribute can be specified more than once. The values are merged
from left to right. A common use case would be to update and/or extend default
classes:
classes = {"btn-primary": True, "btn-secondary": False}
button = html(t'<button class="btn btn-secondary" class={classes}>Click me</button>')
assert str(button) == '<button class="btn btn-primary">Click me</button>'
The style Attribute
The style attribute has special handling to make it easy to combine multiple
styles from different sources. The simplest way is to provide a dictionary of
CSS properties and values for the style attribute:
# Style attributes from dictionaries
styles = {"color": "red", "font-weight": "bold", "margin": "10px"}
styled = html(t"<p style={styles}>Important text</p>")
# <p style="color: red; font-weight: bold; margin: 10px">Important text</p>
Style attributes can also be merged to extend a base style:
add_styles = {"font-weight": "bold"}
para = html(t'<p style="color: red" style={add_styles}>Important text</p>')
assert str(para) == '<p style="color: red; font-weight: bold">Important text</p>'
The data and aria Attributes
The data and aria attributes also have special handling to convert
dictionary keys to the appropriate attribute names:
data_attrs = {"user-id": 123, "role": "admin"}
aria_attrs = {"label": "Close dialog", "hidden": True}
element = html(t"<div data={data_attrs} aria={aria_attrs}>Content</div>")
# <div data-user-id="123" data-role="admin" aria-label="Close dialog"
# aria-hidden="true">Content</div>
Note that boolean values in aria attributes are converted to "true" or
"false" as per the ARIA specification.
Attribute Spreading
It's possible to specify multiple attributes at once by using a dictionary and spreading it into an element using curly braces:
attrs = {"href": "https://example.com", "target": "_blank"}
link = html(t"<a {attrs}>External link</a>")
# <a href="https://example.com" target="_blank">External link</a>
You can also combine spreading with individual attributes:
base_attrs = {"id": "my-link"}
target = "_blank"
link = html(t'<a {base_attrs} target="{target}">Link</a>')
# <a id="my-link" target="_blank">Link</a>
Special attributes likes class behave as expected when combined with
spreading:
classes = {"btn": True, "active": True}
attrs = {"class": classes, "id": "act_now", "data": {"wow": "such-attr"}}
button = html(t'<button {attrs}>Click me</button>')
# <button class="btn active" id="act_now" data-wow="such-attr">Click me</button>
Conditional Rendering
You can use Python's conditional expressions for dynamic content:
is_logged_in = True
user_content = t"<span>Welcome back!</span>"
guest_content = t"<a href='/login'>Please log in</a>"
header = html(t"<div>{user_content if is_logged_in else guest_content}</div>")
# <div><span>Welcome back!</span></div>
Short-circuit evaluation is also supported for conditionally including elements:
show_warning = False
warning = t'<div class="alert">Warning message</div>'
page = html(t"<main>{show_warning and warning}</main>")
# <main></main>
Lists and Iteration
Generate repeated elements using list comprehensions:
fruits = ["Apple", "Banana", "Cherry"]
fruit_list = html(t"<ul>{[t'<li>{fruit}</li>' for fruit in fruits]}</ul>")
# <ul><li>Apple</li><li>Banana</li><li>Cherry</li></ul>
Raw HTML Injection
The tdom package provides several ways to include trusted raw HTML content in
your templates. This is useful when you have HTML content that you know is
safe and do not wish to escape.
Under the hood, tdom builds on top of the familiar
MarkupSafe library to handle trusted
HTML content. If you've used Flask, Jinja2, or similar libraries, this will feel
very familiar.
The Markup class from MarkupSafe is available for use:
from tdom import html, Markup
trusted_html = Markup("<strong>This is safe HTML</strong>")
content = html(t"<div>{trusted_html}</div>")
# <div><strong>This is safe HTML</strong></div>
As a convenience, tdom also supports a :safe format specifier that marks a
string as safe HTML:
trusted_html = "<em>Emphasized text</em>"
page = html(t"<p>Here is some {trusted_html:safe} content.</p>")
# <p>Here is some <em>Emphasized text</em> content.</p>
For interoperability with other templating libraries, any object that implements
a __html__ method will be treated as safe HTML. Many popular libraries
(including MarkupSafe and Django) use this convention:
class SafeWidget:
def __html__(self):
return "<button>Custom Widget</button>"
page = html(t"<div>My widget: {SafeWidget()}</div>")
# <div>My widget: <button>Custom Widget</button></div>
You can also explicitly mark a string as "unsafe" using the :unsafe format
specifier. This forces the string to be escaped, even if it would normally be
treated as safe:
from tdom import html, Markup
trusted_html = Markup("<strong>This is safe HTML</strong>")
page = html(t"<div>{trusted_html:unsafe}</div>")
# <div><strong>This is safe HTML</strong></div>
Template Composition
Related Skills
node-connect
347.2kDiagnose OpenClaw node connection and pairing failures for Android, iOS, and macOS companion apps
claude-opus-4-5-migration
108.0kMigrate prompts and code from Claude Sonnet 4.0, Sonnet 4.5, or Opus 4.1 to Opus 4.5
frontend-design
108.0kCreate distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
model-usage
347.2kUse CodexBar CLI local cost usage to summarize per-model usage for Codex or Claude, including the current (most recent) model or a full model breakdown. Trigger when asked for model-level usage/cost data from codexbar, or when you need a scriptable per-model summary from codexbar cost JSON.
