Supporting t-strings from Python 3.14

t-strings have been merged into Python 3.14 for its beta 1 release (May 7).

Since reading the PEP and its associated example repo, I’ve been noodling about how Django could use t-strings. I am sure others are eager to think about this too. Here are some ideas, I wonder what everyone else thinks.

1. format_html() and format_html_join()

Extend format_html() and format_html_join() to support t-string templates:

from django.utils.html import format_html

format_html(t"Hello <strong>{user.name}</strong>")

This would be a natural extension to these functions. The t-string example in the Python documentation shows a theoretical html function, but provides no implementation. I think it makes sense that we’d make Django’s existing tool support this new syntax.

The documentation does show an “advanced” use case, where a t-string detects a dict argument and expands that into HTML atttributes:

attributes = {"src": "shrubbery.jpg", "alt": "looks nice"}
template = t"<img {attributes}>"
assert html(template) == '<img src="shrubbery.jpg" alt="looks nice" class="looks-nice">'

I’m not sure what others think, but I am cautious about playing with such features in Django, especially the pre-existing format_html(). We can leave the experimentation to other t-string-powered HTML packages. I think supporting the most basic use cases only is sufficient for format_html(): after all, if you want more complexity, you can reach for Django templates.

One note: Previously, we deprecated calling format_html() with one argument, because developers would often put an f-string there without correct escaping (blog post). Adding t-string to suport to format_html() would require us to allow a single-argument form but only if the given argument is a t-string Template.

2. Raw SQL

There are various ways to run raw SQL in Django:

  1. RawSQL
  2. Manager.raw() (and QuerySet.raw())
  3. connection.cursor()

It would be neat if we could support t-strings in these cases too, like:

with connection.cursor() as cursor:
    cursor.execute(t"SELECT * FROM example_book WHERE id = {id}")

I think we could use a simple implementation where we transform templated variables to placeholders, and store variables in a tuple, making the above example equivalent to:

cursor.execute("SELECT * FROM example_book WHERE id = %s", (id,))

I can see one risk here, which is that if the DB-API ends up being extended to support t-strings with some different interface, we might not be able to support that.


Those are the ideas I’ve thought about. Any other suggestions? Maybe translations?

5 Likes

We should definitely play here!

The PEP 750 examples repo has an implementation of that html() function. I don’t know how robust it is, so we’d need to go with care.

The rest of that file looks awfully like the core of a template backend (if we wanted to implement one) :thinking:

Did you not grab thedjango-t-strings PyPI name yet? :winking_face_with_tongue:

1 Like

Thanks a bunch Adam for bringing this up and Carlton for pointing me at this.

First, as catch-up: a LOT has been going on behind the scenes. Including the start of an HTML templating system. I almost have the site ready, but I…don’t. So I’ll share the news here, first time ever: tdom.

  • Written by Andreas from PyScript based on his LONG experience doing this exact thing, repeatedly, in JavaScript frontends
  • Browser-first (specifically, MicroPython friendly)
  • Tooling-friendly

But most of all: the start of a community. “Yay, another templating system.” My hope: we build a community of interoperability, tooling, and middleware. We define a standard Node interface for interchange. A standard component definition about passing props. Whatever.

On to Adam’s points. I want to start with shippable component systems and themes that can be consumed a la carte in Django and/or Jinja2. We don’t have enough high-quality design skill in Python. We should stop spreading that skill across N systems. I’d love for folks to sit in a Django template and pull in well-tested, maintained components in a natural way. Hopefully beyond the __html__ protocol.

I have a PoC for Jinja integration, where components written in a new system get parsed into AST entry points. The hope: people make high-quality, tooling-friendly components that Jinja folks can easily consume. Probably even more natural in Django, which is more friendly to new “tags”.

In fact, what if Django could help invent the new tag/component syntax? Ruff and all the Python tooling would appreciate a standard.

For SQL, Phil Jones sql-tstring that was interesting.

Then after that, there’s a LOT we could all do to bring modern web DX to all of Python. I hope to build a place where we can all discuss, find some agreements, and make a big jump, fast.

4 Likes

@pauleveritt also is on a TalkPython episode TODAY that talks more about this: Episode #505 - t-strings in Python (PEP 750) | Talk Python To Me Podcast

This is great! The maintainer of psycopg is also doing some experiments with t-strings for the psycopg.sql query building module, that I think would likely be very in line with the SQL idea you mention.

Anthony has also has a great video on t-strings.

Picking up the “Maybe translations?” thread-end from @adamchainz — I’ve been working on exactly this, and I think translations may be the most practically valuable t-string lane for Django, because it eliminates a real bug class: .format() applied to the translated string (or forgotten entirely), placeholder typos that only surface in one locale at runtime, and positional placeholders that can’t be reordered.

What works today, with stock gettext machinery, via gettext-tstrings (on PyPI, MIT):

name = user.get_short_name()
_(t"Hello {name}")             # catalog msgid: "Hello {name}"
lazy_gettext(t"Save changes")  # gettext_lazy equivalent, renders per active language

The full sentence goes to the PO catalog; translators can reorder or repeat {name}; attribute access ({user.name}), calls, and translation-side format specs are rejected by a written convention (SPEC.md) enforced identically at extraction and render time. A nice surprise: because these msgids get the standard python-brace-format flag, msgfmt --check-format and Weblate validate placeholder integrity in existing translation pipelines with no configuration.

Two honest Django-specific gaps, which is why I’m posting here rather than claiming this is Django-ready.

Extraction: makemessages wraps xgettext, which cannot parse t-strings. Today extraction goes through Babel (the library ships a babel.extractors entry point). A Django integration would need either a Babel-based extraction step alongside makemessages, or upstream xgettext support — the former exists now, the latter is a longer road.

API shape: Django’s gettext/gettext_lazy accept str. t-string support would want parallel entry points (a Template overload or new names), plural handling via ngettext(t"...", t"...", n), and integration with django.utils.translation’s active-language machinery — the library’s contextvar binding maps onto that cleanly, but the wiring is real design work.

Django’s contributing guidelines ask for new APIs to be validated as third-party packages first — that’s what this is trying to be for t-string i18n. If anyone here wants to experiment with it in a Django project (or tear the msgid convention apart before it fossilizes), I’d value that feedback a lot. For context on the wider discussion: CPython declined stdlib gettext support (gh-137353, with core-dev guidance that this belongs on PyPI), and Babel-native support is under discussion in python-babel/babel issue 1206.

4 Likes