The New Best Way to Format Strings in Python

Python 3.14 introduces t-strings (template strings), a powerful new string formatting mechanism that returns Template objects instead of immediately evaluated strings, enabling safe sanitization, structured logging, and custom templating.

Python has accumulated quite a few ways to format strings over the years. Concatenation with +, percent formatting with %, the .format() method, f-strings, and string.Template. Each new method improved upon the previous one. And now, in Python 3.14, a new contender arrives: t-strings (template strings).

t-strings in Python

Existing String Formatting Methods

Before diving into t-strings, let's briefly recall what we already have:

  • Concatenation with + — the simplest approach, but quickly becomes unreadable
  • Percent formatting % — the oldest method, inherited from C
  • str.format() — more flexible, supports named parameters
  • f-strings — the most popular modern approach, expressions are evaluated immediately
  • string.Template — simple substitution with $variable syntax

What Are T-Strings?

T-strings use the prefix t"..." and support the same expression syntax as f-strings: {expression[!conversion][:format]}. However, unlike f-strings, they don't immediately produce a string. Instead, they return a Template object.

When you write t"Hello, {name}", Python creates a Template object containing:

  • strings — text segments between interpolations
  • values — the computed results of expressions
  • interpolationsInterpolation objects with metadata including the original expression source code, conversion type, and format specifications
name = "World"
template = t"Hello, {name}!"

# template.strings == ("Hello, ", "!")
# template.values == ("World",)
# template.interpolations[0].value == "World"
# template.interpolations[0].expression == "name"

Key Difference from F-Strings

The fundamental distinction is that f-strings evaluate immediately and return a str, while t-strings defer evaluation and return a Template. This deferred evaluation opens up powerful possibilities for libraries and frameworks.

Use Case 1: Sanitization

One of the primary motivations for t-strings is safe handling of potentially dangerous content. Consider SQL injection or XSS attacks — with f-strings, user input is interpolated directly into the string with no opportunity for sanitization.

With t-strings, a library can process all interpolations through a sanitization function before constructing the final string:

from string.templatelib import Template

def sanitize_sql(template: Template) -> str:
    parts = []
    for item in template:
        if isinstance(item, str):
            parts.append(item)
        else:
            # Escape the interpolated value
            parts.append(escape_sql(item.value))
    return "".join(parts)

user_input = "'; DROP TABLE users; --"
query = sanitize_sql(t"SELECT * FROM users WHERE name = '{user_input}'")
# Safe! The dangerous input is escaped

Use Case 2: HTML Templating

HTML frameworks can leverage t-strings to automatically escape interpolations or parse them into structured elements, supporting safer component-based rendering:

def html(template: Template) -> SafeHTML:
    parts = []
    for item in template:
        if isinstance(item, str):
            parts.append(item)
        else:
            parts.append(html_escape(str(item.value)))
    return SafeHTML("".join(parts))

user_name = "<script>alert('xss')</script>"
page = html(t"<div>Welcome, {user_name}!</div>")
# The script tag is safely escaped

Use Case 3: Structured Logging

T-strings enable creating machine-readable logs automatically by extracting field names and values from template expressions. The log message signature is generated by replacing interpolations with ? markers:

def log_structured(template: Template) -> dict:
    message_parts = []
    fields = {}
    for item in template:
        if isinstance(item, str):
            message_parts.append(item)
        else:
            message_parts.append("?")
            fields[item.expression] = item.value
    return {
        "message": "".join(message_parts),
        "fields": fields
    }

request_id = 42
user = "alice"
log = log_structured(t"Request {request_id} from {user}")
# {"message": "Request ? from ?", "fields": {"request_id": 42, "user": "alice"}}

Implementation Details

Under the hood, t-strings compile to bytecode operations: BUILD_INTERPOLATION and BUILD_TEMPLATE. These construct Template objects at runtime. Format specifications control whether conversion and formatting data are included on the stack.

It's worth noting that t-strings are significantly slower than standard str.format due to the overhead of creating Template and Interpolation objects. However, for use cases where safety and customization outweigh raw performance, this tradeoff is worthwhile.

Design Philosophy

T-strings separate "what to format" from "how to format it." This enables libraries to customize formatting behavior while keeping syntax clean and supporting IDE tooling like autocompletion and type checking. The syntax is intentionally identical to f-strings so that migrating from f"..." to t"..." requires changing just one character.

T-string diagram

Conclusion

T-strings represent a significant evolution in Python's string formatting capabilities. While f-strings remain the best choice for simple string construction, t-strings open the door to safe, structured, and customizable string processing that was previously difficult or impossible to achieve cleanly.