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.

Python's new t-strings

Template strings, also known as t-strings, have been officially accepted as a feature in Python 3.14, which will ship in October 2025. 🎉

I’m excited about t-strings because they make string processing safer and more flexible. In this post, I’ll explain what t-strings are, why they were added to Python, and how you can use them.

What are t-strings?

Template strings are like f-strings with superpowers. They share the same familiar syntax:

food = "cheese"
string = f"Tasty {food}!"
template = t"Tasty {food}!"

But they result in different types:

food = "cheese"
type(f"Tasty {food}!")
# <class 'str'>
type(t"Tasty {food}!")
# <class 'string.templatelib.Template'>

F-strings are “just” strings. But t-strings give you a new type, Template, that lets you access the parts of your string:

food = "cheese"
template = t"Tasty {food}!"
list(template)
# ['Tasty ', Interpolation(value='cheese'), '!']

The Interpolation type is also new in Python 3.14. It’s a fancy way of saying “this part of your string was created by substitution.”

By giving developers access to the parts of strings, t-strings make it possible to write code that processes strings in powerful and safe ways.

Why t-strings?

Since they were introduced in Python 3.6, f-strings have become very popular. Developers use them for everything… even when they shouldn’t.

The most common (mis)uses of f-strings lead to security vulnerabilities like SQL injection and cross-site scripting (XSS).

Here’s the famous “Little Bobby Tables” vulnerability using f-strings:

from my_database_library import execute_sql

def get_student(name: str) -> dict:
    query = f"SELECT * FROM students WHERE name = '{name}'"
    return execute_sql(query)

get_student("Robert'); DROP TABLE students;--")  # ☠️ ☠️ ☠️

That execute_sql() function takes a str as input. It has no way to know whether that Robert'); nonsense was intended or not. (It wasn’t.)

Template strings solve this problem because they keep track of which parts of a string are static and which parts are dynamic. This makes it possible to write libraries that safely process user input. An updated version of the database library could look like this:

from string.templatelib import Template
from my_database_library import execute_sql_t

def get_student(name: str) -> dict:
    query = t"SELECT * FROM students WHERE name = '{name}'"
    return execute_sql_t(query)

get_student("Robert'); DROP TABLE students;--")  # 🎉 🦄 👍

No more SQL injection! The execute_sql_t() function can see that {name} is a substitution and can safely escape its value.

Importantly, Template instances are not strings. They cannot be used in places that expect a str. They cannot be concatenated with plain strings. They offer no specialized __str__() implementation; str(template) is not useful. Instead, if you want to convert a Template to a str, you must explicitly process it first.

We can imagine a library that provides an html() function that takes a Template and returns a safely escaped string:

user_supplied_input = "<script>alert('pwn')</script>"
safe = html(t"<p>{user_supplied_input}</p>")
assert safe == "<p>&lt;script&gt;alert('pwn')&lt;/script&gt;</p>"

Of course, t-strings are useful for more than just safety; they also allow for more flexible string processing. For example, that html() function could return a new type, Element. It could also accept all sorts of useful substitutions in the HTML itself:

attributes = {"src": "roquefort.jpg", "alt": "Yum"}
element = html(t"<img {attributes} />")
# You can imagine Element having a nice __str__ method:
assert str(element) == "<img src='roquefort.jpg' alt='Yum' />"

If you’ve worked with JavaScript, t-strings may feel familiar. They are the pythonic parallel to JavaScript’s tagged templates.

How do I work with t-strings?

The easiest way to get access to the parts of a Template is to iterate over it:

food = "cheese"
template = t"Tasty {food}!"
for part in template:
    if isinstance(part, str):
        print("String part:", part)
    else:
        print("Interpolation part:", part.value)
# String part: Tasty
# Interpolation part: cheese
# String part: !

You can also access the parts directly via the .strings and .values properties:

food = "cheese"
template = t"Tasty {food}!"
assert template.strings == ("Tasty ", "!")
assert template.values == (food,)

There is always one more (possibly empty) string than value. That is, t"".strings == ("",) and t"{food}".strings == ("", "").

Developers writing complex processing code can also access the gory details of each interpolation:

food = "cheese"
template = t"Tasty {food!s:>8}!"
interpolation = template.interpolations[0]
assert interpolation.value == "cheese"
assert interpolation.expression == "food"
assert interpolation.conversion == "s"
assert interpolation.format_spec == ">8"

If you’ve gone deep with f-strings in the past, you may recognize that conversion and format_spec correspond to the !s and :>8 parts of the f‑string syntax.

The expression property is a string containing the exact expression that was used inside the curly braces. This means you can use ast.parse() to analyze it further if you need to.

Most of the time, you’ll probably create Template instances using the t"..." literal syntax. But sometimes, you may want to create them programmatically:

from string.templatelib import Template, Interpolation
template = Template("Tasty ", Interpolation("cheese"), "!")

Strings and interpolations can be provided to the Template constructor in any order.

Processing t-strings

Let’s say we wanted to write code to convert all substituted words into pig latin. All it takes is a simple function:

def pig_latin(template: Template) -> str:
    """Convert a Template to pig latin."""
    result = []
    for item in template:
        if isinstance(item, str):
            result.append(item)
        else:
            word = item.value
            if word and word[0] in "aeiou":
                result.append(word + "yay")
            else:
                result.append(word[1:] + word[0] + "ay")
    return "".join(result)

food = "cheese"
template = t"Tasty {name}!"
assert pig_latin(template) == "Tasty heesecay!"

This is a goofy example; if you’d like to see some less silly examples, check out the PEP 750 examples repository.

What’s next once t-strings ship?

T-strings are a powerful new feature that will make Python string processing safer and more flexible. I hope to see them used in all sorts of libraries and frameworks, especially those that deal with user input.

In addition, I hope that the tooling ecosystem will adapt to support t-strings. For instance, I’d love to see black and ruff format t-string contents, and vscode color those contents, if they’re a common type like HTML or SQL.

It’s been fun to get to know and work with Jim, Paul, Koudai, Lysandros, and Guido on this project and to interact with many more members of the Python community online without whose input PEP 750 simply wouldn’t have come together. I can’t wait to see what developers build with t-strings once they ship!