Introducing tdom: HTML templating with t‑strings

Python 3.14’s new t‑strings add flexibility and power to the language’s arsenal of string processing tools. They make it easy to distinguish between static and dynamic content—essential for safe web templating.

That’s why we’re excited to introduce tdom, a brand-new toolkit that leverages t‑strings and brings Python web templates into the modern era. It’s easy to use: write HTML in a t‑string and pass it to tdom.html():

>>> from tdom import html
>>>
>>> name = "Alice"
>>> node = html(t'<div>Hi, {name}!</div>')
>>> str(node)
'<div>Hi, Alice!</div>'

Under the hood, html() parses your HTML and returns a tree of nodes. These can be manipulated, inspected, or rendered with str().

As you’d expect, html() safely escapes special characters found in substitutions:

>>> from tdom import html
>>>
>>> name = '<script>alert("pwned")</script>'
>>> node = html(t'<div>Hi, {name}!</div>')
>>> str(node)
'<div>Hi, &lt;script&gt;alert("pwned")&lt;/script&gt;!</div>'

In addition to keeping things safe, tdom provides a wide variety of convenience features that make it easier to build complex HTML. If you’ve ever used lit-html, htm, JSX, or similar JavaScript tools, you’ll feel right at home.

To give a taste of what’s possible, here are a few highlights:

  • Dynamic attributes: Provide individual attributes or many at once with a dict. Boolean attributes are handled the way you’d expect.

    >>> from tdom import html
    >>>
    >>> attrs = {"id": "yum", "data-act": "submit"}
    >>> disabled = True
    >>> t = t'<button {attrs} disabled={disabled}>Yum!</button>'
    >>> node = html(t)
    >>> str(node)
    '<button id="yum" data-act="submit" disabled>Yum!</button>'
  • Special handling for special attributes: Deftly deal with data-*, aria-*, style and class attributes. Inspired by classnames, CSS classes can be provided as strings, lists, or dicts.

    >>> from tdom import html
    >>>
    >>> classes = ["btn", {"primary": False}, {"active": True}]
    >>> node = html(t'<button class={classes}>Click me</button>')
    >>> str(node)
    '<button class="btn active">Click me</button>'
  • Intuitive content: Pass strings, nodes, iterables, and much more as child content; tdom will flatten and render it all correctly.

    >>> from tdom import html
    >>>
    >>> cheeses = ["Cheddar", "Gouda", "Brie"]
    >>> items = (t'<li>{cheese}</li>' for cheese in cheeses)
    >>> node = html(t'<ul>{items}</ul>')
    >>> str(node)
    '<ul><li>Cheddar</li><li>Gouda</li><li>Brie</li></ul>'
  • Components: Define reusable functions and classes and invoke them directly in your templates using a JSX-like syntax.

    >>> from tdom import html, Node
    >>>
    >>> def Card(
    ...     children: Node,
    ...     title: str,
    ...     subtitle: str | None = None,
    ...     **attrs,
    ... ) -> Node:
    ...     return html(t'''
    ...       <div class="card" {attrs}>
    ...         <h2>{title}</h2>
    ...         {subtitle and t'<h3>{subtitle}</h3>'}
    ...         <div class="content">
    ...           {children}
    ...         </div>
    ...       </div>''')
    >>>
    >>> result = html(t'''
    ...   <{Card} title="Cheese Emporium" id="cheeses">
    ...     <p>Sorry, we're fresh out!</p>
    ...   </{Card}>''')
    >>>
    >>> str(result)
    <div class="card" id="cheeses">
      <h2>Cheese Emporium</h2>
      <div class="content">
        <p>Sorry, we're fresh out!</p>
      </div>
    </div>
  • Much more. If it feels intuitive, it’s probably supported… or should be! See the README for a rundown of current features.

Please give tdom a try and send us your feedback! If you have Astral’s uv installed, you can use tdom today with:

uv run --with tdom --python 3.14 python

The tdom project is still very young; it’s labeled pre-alpha on PyPI and there’s still plenty of work for us to do. If you run into bugs, please open an issue; if there’s an intuitive feature you’d like to see that we don’t yet support, please start a discussion and we’ll find a way to make it happen.

tdom is part of a broader ecosystem of tools designed to promote modern HTML templating in Python. Early efforts are underway to add syntax highlighting, formatting, linting, and autocompletion support to popular editors like VS Code and PyCharm. Stay tuned for updates!

A camera-friendly pika perched on the rocks along the Chain Lakes Loop near Mount Baker.
A camera-friendly pika perched on the rocks along the Chain Lakes Loop near Mount Baker.

The premiere of Python: The Documentary is just a couple hours away. It streams for free on YouTube at 10AM Pacific.

Over the past year, I’ve had the pleasure of getting to know and collaborate with a few of the Pythonistas involved. I’m excited to learn more about the history and growth of Python’s welcoming and impactful open source community.

“I built an entire startup with AI and now it’s profitable.”

Posts like this seem increasingly common. Curiously, they rarely include details: a link to the startup, a clear description of the customer and problem, or a breakdown of how AI was actually used.

Please allow me to admit my gentle skepticism.

I use LLMs to write code. Like anyone who has done so, I’ve learned a lot about when they’re useful and when they’re… hopeless. Claims to the contrary aside, they can’t do it all. They can’t even do it most.

I’ve also helped build (and tragicomically failed to build) a few businesses. Like anyone who has done so, I know tech is rarely the long pole in the tent. Sales, marketing, building a team, crafting a culture, instilling values, listening to customers and responding to their needs, making forward progress in a sea of uncertainty, getting anyone to care at all? Hard. And, last I checked, not things AI can singlehandedly solve.

All this said: it is an exciting time to be a solopreneur. Used judiciously, I believe that AI can help builders move faster. I look forward to a future full of delightful, nuanced stories about how small teams leveraged AI to craft something people love. Just be sure to include a link.

Unsheltered Seattle

I built a little website to help wrap my head around the state of unsheltered homelessness in Seattle.

The site uses Seattle’s public customer service request dataset, which aggregates data from the Find It, Fix It App and city web portal to show recent (and, if you like, historical) reports of encampments, abandoned vehicles, and related issues:

Through the magic of GitHub actions, the website’s data is updated daily. I’ve learned that the location of encampments can change rapidly, sometimes as a natural consequence of people moving around, and sometimes due to aggressive sweeps by the city. The site shows timelines for major encampments so you can get a sense of how they’ve evolved.

The visualization also introduces the idea of “safe zones”: areas in the city that fall within roughly a block of public parks, schools, libraries, and child care centers. These closely match the zones that Portland has established as part of Mayor Wilson’s homelessness strategy. (Seattle has not established such zones and I’m not an advocate, but I think it’s worth understanding how they work.) For instance, you can use Unsheltered Seattle to filter down to highly reported abandoned vehicles parked near schools.

It’s worth noting that report counts, while directionally useful, don’t precisely indicate the number of tents or unsheltered individuals. Seattleites have to be motivated to submit reports. Different neighborhoods have different levels of engagement with the Find It, Fix It app, and some encampments are more visible or in more pedestrian-heavy areas than others. Just one tent in a prominent location along the Burke-Gilman trail is likely to gather more reports than twenty tents in a secluded slope at the back of the Queen Anne Greenbelt. The data also shows clear seasonal and short-term weather related patterns.

The site is open source, so if you’re curious about how it works, you can check out the GitHub repository. It’s a complete mess at the moment since I extracted it from a larger previous project. Hopefully I’ll have time to clean it up a bit in the future.

There are bugs and then there are bugs.

The latest Oxide and Friends episode tells the story of an epic data corruption bug that nearly derailed Oxide’s launch.

Says CTO Bryan Cantrill:

An adage of mine is that bugs can be psychotic or nonreproducible, but not both.

And when I say psychotic I don’t mean just, like, difficult. I mean […] ripping at the fabric of reality.

The fabric of reality that is created by the computer, by the operating system, it creates these abstractions that we view as kind of bedrock abstractions. And when those start to break, that’s a psychotic bug. You know, when you have a thread that is executing on two CPUs at the same time? It’s like: it can’t be.

Such a good listen.

My daughter and I vibe coded a little Cat Restaurant Game using VSCode, GitHub Copilot in its new-ish agent mode, and Claude Sonnet 4. The graphics were generated by ChatGPT; my daughter made the sounds and music using GarageBand.

You play a little girl that runs a sushi restaurant and has to feed the hungry cat customers before they get impatient:

The whole project took about three hours over two sessions. We never touched the code. Instead, the exercise was to take a vague game idea, make it concrete, break it into sub-parts that formed a coherent whole, and then describe each part in sufficient detail to get Copilot to generate the outcome we wanted. In other words, a great learning experience!

Because we never touched the code, it’s predictably wonky. For instance, it uses requestAnimationFrame() but fails to make use of the time delta in callbacks; I assume the game will run at different speeds on different machines. Another example: the GameState interface is a mishmash, confusing game state with input state among other things. But hey, the thing works!

One of my favorite database design patterns is what I call the “mullet schema”: business up front, party in the back, where every table in the database ends with a JSON column.

I even mentioned it on a recent episode of the Talk Python podcast:

This morning, I read David Crawshaw’s excellent post, How I Program With Agents. In a sidebar, he completely blew my mullet-addled mind:

I learned an odd way of using SQL at Tailscale (from Brad and Maisem): make every table a JSON object. In particular, have only one “real” column and the rest generated from the JSON. So the typical table looks like:

CREATE TABLE IF NOT EXISTS Cookie (
  Cookie   TEXT    NOT NULL AS (Data->>'cookie')  STORED UNIQUE, -- PK
  UserID   INTEGER NOT NULL AS (Data->>'user_id') STORED REFERENCES User (UserID),
  Created  INTEGER NOT NULL AS (unixepoch(Data->>'created')) STORED,
  LastUsed INTEGER AS (unixepoch(Data->>'last_used')) CHECK (LastUsed>0),
  Data     JSONB   NOT NULL
);

Now it’s a party!

David mentions some of the tradeoffs and hints at a potential future blog post about the approach. In the meantime, I may need to rethink my mullet nomenclature!

I gave a lightning talk at PyCon introducing t-strings to a hall full of Pythonistas. At just under five minutes, the intro had to be a “whirlwind”. I hope I managed to convey the basics in a fun way.

PS: right after I spoke, Sheena O’Connell gave a talk about Python in Africa that I found inspiring. It’s worth a watch.

I just ran across* Will Vincent’s delightful PyCon 2025 Recap, the first of many recaps I hope to read from folks I met at the conference.

* I’m old, so I “ran across” it in my RSS reader.

LLMs are powerful levers for writing code. With time and plenty of experimentation, I’ve found tools and strategies that work well for me.

LLMs are also wildly flexible. To learn how others use them, I’ve started watching screencasts by expert engineers. Three I recommend:

All three have very different styles; I’ve learned quite a bit from each. Check out their channels and see what vibes with you.

The view from behind Covel Creek Falls, during a weekend campout in the Gifford Pinchot National Forest.
The view from behind Covel Creek Falls, during a weekend campout in the Gifford Pinchot National Forest.