Intermediate

Python 3.14 introduces a powerful new feature called T-Strings (Template Strings) that revolutionizes how you handle string interpolation. If you’ve been frustrated with the limitations of f-strings when dealing with security-sensitive operations like database queries or HTML generation, T-Strings offer an elegant solution. Unlike traditional f-strings that immediately evaluate and return strings, T-Strings return Template objects that give you fine-grained control over how values are processed.

Don’t worry if you’re new to the concept–by the end of this tutorial, you’ll understand exactly when and how to use T-Strings in your projects. We’ll start with practical examples, explore the underlying mechanisms, and then dive into real-world use cases like SQL injection prevention and HTML escaping.

In this guide, we’ll cover the syntax of T-Strings, the Template protocol they implement, how to process templates with custom functions, and several practical applications that will make your code more secure and maintainable. We’ll also explore how this feature compares to existing string formatting methods in Python.

Quick Example

Before diving into the theory, let’s see T-Strings in action. This example demonstrates the fundamental difference between f-strings and T-Strings:

# quick_tstring_demo.py
from __future__ import annotations

# T-String creates a Template object, not a plain string
user_input = "Robert'; DROP TABLE students; --"
database_query = t"SELECT * FROM users WHERE id = {user_input}"

print(f"Template type: {type(database_query)}")
print(f"Template strings attr: {database_query.strings}")
print(f"Template values attr: {database_query.values}")

# You can process the template safely
def escape_sql_value(value):
    """Escape value for SQL injection prevention"""
    escaped = str(value).replace("'", "''")
    return f"'{escaped}'"

# Process the template and build the safe query
safe_parts = [database_query.strings[0]]
for i, value in enumerate(database_query.values):
    safe_parts.append(escape_sql_value(value))
    safe_parts.append(database_query.strings[i + 1])

final_query = "".join(safe_parts)
print(f"\nFinal query: {final_query}")

Output:

Template type: 
Template strings attr: ['SELECT * FROM users WHERE id = ', '']
Template values attr: ["Robert'; DROP TABLE students; --"]

Final query: SELECT * FROM users WHERE id = 'Robert''; DROP TABLE students; --'

Notice how the T-String defers the actual string assembly, allowing us to sanitize values before they’re combined. This is the core advantage of T-Strings over f-strings.

What Are T-Strings?

T-Strings, defined in PEP 750, introduce a new string prefix `t` that transforms string literals into Template objects instead of plain strings. This small change has significant implications for security and control over string interpolation.

Here’s a comparison of different string formatting approaches in Python:

Approach Returns Evaluated At Best Use Case
f-string: `f”Hello {name}”` str Statement time Simple output, logging
T-String: `t”Hello {name}”` Template Never (deferred) Security-sensitive, custom processing
.format(): `”Hello {}”.format(name)` str Call time Legacy code, simple formatting
% formatting: `”Hello %s” % name` str Call time Very old codebases

The key insight is that T-Strings separate the specification of a template (which values go where) from the actual interpolation of values into the template. This separation allows you to apply custom processing logic before combining strings and values.

T-String Basic Syntax

Creating Template Objects

The syntax for creating a T-String is straightforward–use the `t` prefix just like you would use `f` for an f-string:

# tstring_syntax.py
from __future__ import annotations

# Basic T-String
product = "Laptop"
price = 1299.99

template = t"Product: {product}, Price: ${price}"

# Check the type
print(f"Type: {type(template)}")
print(f"Is it a Template? {type(template).__name__}")

# Access the components
print(f"Strings: {template.strings}")
print(f"Values: {template.values}")

Output:

Type: 
Is it a Template? Template
Strings: ('Product: ', ', Price: $', '')
Values: ('Laptop', 1299.99)

The Template object stores the literal string parts and the values separately. The `strings` attribute is a tuple of the static parts, and `values` is a tuple of the interpolated values.

Accessing Template Parts

Once you have a Template object, you can inspect its structure in detail. Templates provide multiple ways to access their components:

# template_inspection.py
from __future__ import annotations

username = "alice"
timestamp = "2026-04-01 14:30:00"
template = t"User {username} logged in at {timestamp}"

# Direct attribute access
print("Strings part:", template.strings)
print("Values part:", template.values)

# Using .args for detailed interpolation info
print("\nInterpolation details:")
for interpolation in template.args:
    print(f"  Value: {interpolation.value}")
    print(f"  Expression: {interpolation.expression}")
    print(f"  Conversion: {interpolation.conversion}")
    print(f"  Format spec: {interpolation.format_spec}")
    print()

Output:

Strings part: ('User ', ' logged in at ', '')
Values part: ('alice', '2026-04-01 14:30:00')

Interpolation details:
  Value: alice
  Expression: username
  Conversion: None
  Format spec: ''

  Value: 2026-04-01 14:30:00
  Expression: timestamp
  Conversion: None
  Format spec: ''

The Interpolation objects give you access to the original expression as a string, the conversion flag (if any), and the format specification. This information is crucial when building custom processors.

The Template Protocol

Implementing the Template Protocol

Python defines a formal Template protocol that allows custom objects to work with the `__format__()` method. When you want to create custom template processors, you implement this protocol. The Template protocol requires implementing methods to process template objects systematically.

# custom_template_processor.py
from __future__ import annotations
from typing import Any

class SecureFormatter:
    """Processor that escapes all template values for safe output"""

    def __init__(self, escape_func):
        self.escape_func = escape_func

    def __call__(self, template):
        """Process a template object and return a safe string"""
        result_parts = []

        # Start with the first literal string
        result_parts.append(template.strings[0])

        # Process each value with the escape function
        for i, value in enumerate(template.values):
            escaped_value = self.escape_func(value)
            result_parts.append(str(escaped_value))
            result_parts.append(template.strings[i + 1])

        return "".join(result_parts)

# Define an HTML escaping function
def escape_html(value):
    """Escape HTML special characters"""
    replacements = {
        "&": "&",
        "<": "<",
        ">": ">",
        '"': """,
        "'": "'"
    }
    result = str(value)
    for char, escaped in replacements.items():
        result = result.replace(char, escaped)
    return result

# Use the processor
html_formatter = SecureFormatter(escape_html)
user_comment = ""
template = t"User comment: {user_comment}"
safe_html = html_formatter(template)
print(f"Safe output: {safe_html}")

Output:

Safe output: User comment: <script>alert('XSS')</script>

This example shows how you can create a custom processor that takes any escape function and applies it uniformly to all values in a template. This is one of the primary strengths of T-Strings over f-strings.

Processing Templates with Custom Functions

Building Safe Template Processors

Now let’s build more sophisticated template processors for real-world scenarios. The key is to leverage the separation of strings and values that T-Strings provide:

# advanced_processors.py
from __future__ import annotations
from typing import Callable

class TemplateProcessor:
    """Base processor for handling template objects"""

    def __init__(self, value_handler: Callable):
        self.value_handler = value_handler

    def process(self, template):
        """Process a template with custom handling for each value"""
        parts = [template.strings[0]]

        for i, value in enumerate(template.values):
            processed_value = self.value_handler(value)
            parts.append(str(processed_value))
            parts.append(template.strings[i + 1])

        return "".join(parts)

# Example 1: JSON safe processor
def jsonify(value):
    """Convert value to JSON-safe representation"""
    if isinstance(value, str):
        return f'"{value.replace('"', '\\"')}"'
    elif isinstance(value, bool):
        return "true" if value else "false"
    elif value is None:
        return "null"
    else:
        return str(value)

# Example 2: URL safe processor (basic)
def urlencode(value):
    """Simple URL encoding"""
    import urllib.parse
    return urllib.parse.quote(str(value))

# Create processors
json_processor = TemplateProcessor(jsonify)
url_processor = TemplateProcessor(urlencode)

# Use cases
config_data = '{"admin": true}'
search_term = "python 3.14"

json_template = t"var data = {config_data};"
url_template = t"https://example.com/search?q={search_term}"

print("JSON output:", json_processor.process(json_template))
print("URL output:", url_processor.process(url_template))

Output:

JSON output: var data = "{"admin": true}";
URL output: https://example.com/search?q=python%203.14

By encapsulating the processing logic, you create reusable processors that can handle multiple templates with consistent behavior.

SQL Injection Prevention with T-Strings

Why SQL Injection Prevention Matters

SQL injection is one of the most critical security vulnerabilities in web applications. It occurs when untrusted input is concatenated directly into SQL queries. T-Strings provide an elegant mechanism to prevent this by forcing you to process all values before they enter your SQL.

# sql_safe_queries.py
from __future__ import annotations

class SQLQueryBuilder:
    """Safe SQL query builder using T-Strings"""

    def __init__(self):
        self.query_parts = []

    @staticmethod
    def escape_value(value):
        """Escape values for SQL"""
        if value is None:
            return "NULL"
        elif isinstance(value, bool):
            return "TRUE" if value else "FALSE"
        elif isinstance(value, (int, float)):
            return str(value)
        else:
            # Escape single quotes
            escaped = str(value).replace("'", "''")
            return f"'{escaped}'"

    def build_from_template(self, template):
        """Build a safe query from a T-String template"""
        query_parts = [template.strings[0]]

        for i, value in enumerate(template.values):
            escaped = self.escape_value(value)
            query_parts.append(escaped)
            query_parts.append(template.strings[i + 1])

        return "".join(query_parts)

# Example usage with dangerous input
builder = SQLQueryBuilder()

# Safe input
user_id = 42
query1 = builder.build_from_template(t"SELECT * FROM users WHERE id = {user_id}")
print("Safe query:", query1)

# Dangerous input that would fail with f-strings
email = "test@example.com'; DELETE FROM users; --"
query2 = builder.build_from_template(t"SELECT * FROM users WHERE email = {email}")
print("Protected query:", query2)

Output:

Safe query: SELECT * FROM users WHERE id = 42
Protected query: SELECT * FROM users WHERE email = 'test@example.com''; DELETE FROM users; --'

Notice how the dangerous SQL injection attempt is neutralized–the single quote in the input is escaped to two quotes, and the entire value is wrapped in quotes, making it a literal string value rather than executable SQL code.

Using T-Strings with Parameterized Queries

For database operations, you typically want to use parameterized queries (prepared statements) rather than string concatenation. T-Strings make this cleaner:

# parameterized_queries.py
from __future__ import annotations

class ParameterizedQueryBuilder:
    """Build parameterized queries using T-Strings"""

    def build_query(self, template):
        """Extract placeholders and parameters from T-String"""
        placeholders = []
        parameters = []

        for i, value in enumerate(template.values):
            placeholders.append(f"${i + 1}")  # PostgreSQL style
            parameters.append(value)

        # Reconstruct query with placeholders
        query_parts = [template.strings[0]]
        for i, placeholder in enumerate(placeholders):
            query_parts.append(placeholder)
            query_parts.append(template.strings[i + 1])

        query = "".join(query_parts)
        return query, tuple(parameters)

# Usage
builder = ParameterizedQueryBuilder()

user_id = 42
email = "alice@example.com"
template = t"SELECT * FROM users WHERE id = {user_id} AND email = {email}"

query, params = builder.build_query(template)
print(f"Query: {query}")
print(f"Parameters: {params}")

# This would then be executed as: cursor.execute(query, params)

Output:

Query: SELECT * FROM users WHERE id = $1 AND email = $2
Parameters: (42, 'alice@example.com')

Using parameterized queries is the gold standard for SQL safety, and T-Strings make it easy to construct these queries while keeping your code readable.

HTML Escaping and Content Security

Preventing Cross-Site Scripting (XSS) Attacks

Just as T-Strings help prevent SQL injection, they’re equally valuable for preventing XSS attacks when generating HTML. The process is identical–escape user input before it enters the template output:

# html_template_rendering.py
from __future__ import annotations
import html

class HTMLRenderer:
    """Render HTML templates safely with T-Strings"""

    @staticmethod
    def render(template):
        """Render T-String template as safe HTML"""
        parts = [template.strings[0]]

        for i, value in enumerate(template.values):
            # Use html.escape for automatic entity encoding
            escaped = html.escape(str(value))
            parts.append(escaped)
            parts.append(template.strings[i + 1])

        return "".join(parts)

# Example: User-generated content
renderer = HTMLRenderer()

username = ""
user_bio = "I love "

welcome_html = renderer.render(t"

Welcome, {username}!

") bio_html = renderer.render(t"

{user_bio}

") print("Rendered welcome:", welcome_html) print("Rendered bio:", bio_html)

Output:

Rendered welcome: 

Welcome, <img src=x onerror='alert("XSS")'>!

Rendered bio:

I love <script>alert('hack')</script>

The `html.escape()` function automatically converts dangerous characters like `<`, `>`, and quotes into their HTML entity equivalents. Combined with T-Strings, this creates a clean, declarative way to generate safe HTML from user input.

Building Safe Template Systems

For more complex HTML generation, you can build template systems that enforce safety at the framework level:

# safe_template_system.py
from __future__ import annotations
import html

class SafeHTMLTemplate:
    """A template class that automatically escapes all interpolations"""

    def __init__(self, content_template):
        self.template = content_template
        self.escaper = html.escape

    def render(self):
        """Render the template with automatic escaping"""
        parts = [self.template.strings[0]]

        for i, value in enumerate(self.template.values):
            escaped_value = self.escaper(str(value))
            parts.append(escaped_value)
            parts.append(self.template.strings[i + 1])

        return "".join(parts)

# Usage in a web framework context
user_data = {
    "name": "Alice",
    "title": "Admin",
    "bio": "Python "
}

# Create safe templates
name_template = SafeHTMLTemplate(t"{user_data['name']}")
title_template = SafeHTMLTemplate(t"

{user_data['title']}

") bio_template = SafeHTMLTemplate(t"

{user_data['bio']}

") # Render all safely print(name_template.render()) print(title_template.render()) print(bio_template.render())

Output:

Alice

<b>Admin</b>

Python <developer>

Notice how the HTML markup in the user data is escaped, preventing any script injection while preserving the intended content.

Advanced: Custom Processors and DSLs

Building Domain-Specific Languages

T-Strings enable the creation of domain-specific languages (DSLs) by allowing custom processing of templates. For example, you could build a templating language for configuration files, data validation, or custom syntax:

# custom_dsl_processor.py
from __future__ import annotations

class ConfigProcessor:
    """Process T-Strings as configuration templates"""

    def __init__(self):
        self.variables = {}

    def register_variable(self, name, value):
        """Register a variable for substitution"""
        self.variables[name] = value

    def process_template(self, template):
        """Process template with variable replacement and formatting"""
        parts = [template.strings[0]]

        for i, value in enumerate(template.values):
            # Apply custom processing based on type
            if isinstance(value, bool):
                processed = "yes" if value else "no"
            elif isinstance(value, (list, tuple)):
                processed = ", ".join(str(v) for v in value)
            elif isinstance(value, dict):
                processed = "; ".join(f"{k}={v}" for k, v in value.items())
            else:
                processed = str(value)

            parts.append(processed)
            parts.append(template.strings[i + 1])

        return "".join(parts)

# Usage in a configuration context
processor = ConfigProcessor()

debug_mode = True
log_level = "INFO"
features = ["auth", "api", "websocket"]
db_config = {"host": "localhost", "port": 5432}

config_template = t"""
Debug mode: {debug_mode}
Log level: {log_level}
Enabled features: {features}
Database config: {db_config}
"""

config_output = processor.process_template(config_template)
print("Generated config:")
print(config_output)

Output:

Generated config:
Debug mode: yes
Log level: INFO
Enabled features: auth, api, websocket
Database config: host=localhost; port=5432

This demonstrates how T-Strings allow you to build sophisticated text generation systems with custom rules for different data types.

Real-World Example: Building a Log Formatter

Let’s build a practical logging system that uses T-Strings to format log messages with automatic context escaping and structuring:

# structured_logging.py
from __future__ import annotations
import json
from datetime import datetime
from enum import Enum

class LogLevel(Enum):
    DEBUG = "DEBUG"
    INFO = "INFO"
    WARNING = "WARNING"
    ERROR = "ERROR"

class StructuredLogger:
    """Logger that formats messages using T-Strings"""

    def __init__(self, service_name):
        self.service_name = service_name
        self.logs = []

    def _create_log_entry(self, level, template):
        """Create a structured log entry from a T-String template"""
        # Build the message
        message_parts = [template.strings[0]]
        context = {}

        for i, value in enumerate(template.values):
            # Use expression as context key
            interpolation = template.args[i]
            key = interpolation.expression or f"arg{i}"

            # Store in context dict
            context[key] = value

            # Add to message
            message_parts.append(str(value))
            message_parts.append(template.strings[i + 1])

        message = "".join(message_parts)

        # Create structured log entry
        log_entry = {
            "timestamp": datetime.now().isoformat(),
            "service": self.service_name,
            "level": level.value,
            "message": message,
            "context": context
        }

        return log_entry

    def log(self, level, template):
        """Log a message with automatic context capture"""
        entry = self._create_log_entry(level, template)
        self.logs.append(entry)
        print(json.dumps(entry, indent=2))

# Usage
logger = StructuredLogger("auth-service")

user_id = 12345
username = "alice_smith"
ip_address = "192.168.1.100"

logger.log(LogLevel.INFO, t"User {username} (ID: {user_id}) logged in from {ip_address}")

failed_attempts = 5
max_attempts = 10
logger.log(LogLevel.WARNING, t"User {username} has {failed_attempts} failed login attempts (max: {max_attempts})")

Output:

{
  "timestamp": "2026-04-01T12:30:45.123456",
  "service": "auth-service",
  "level": "INFO",
  "message": "User alice_smith (ID: 12345) logged in from 192.168.1.100",
  "context": {
    "username": "alice_smith",
    "user_id": 12345,
    "ip_address": "192.168.1.100"
  }
}
{
  "timestamp": "2026-04-01T12:30:46.234567",
  "service": "auth-service",
  "level": "WARNING",
  "message": "User alice_smith has 5 failed login attempts (max: 10)",
  "context": {
    "username": "alice_smith",
    "failed_attempts": 5,
    "max_attempts": 10
  }
}

This example shows how T-Strings enable automatic context extraction and structured logging, capturing not just the formatted message but also the individual values and their names for later analysis.

How to Try Python 3.14 T-Strings Today

Since Python 3.14 is still in development as of this writing, you’ll need to run Python from the development version. Here’s how to get started:

# Installation options for trying T-Strings

# Option 1: Build from source (Linux/macOS)
git clone https://github.com/python/cpython.git
cd cpython
./configure --prefix=$HOME/python314
make
make install

# Option 2: Use Docker
docker run -it python:3.14-dev bash

# Option 3: Download pre-built alpha/beta releases
# Visit https://www.python.org/downloads/
# Look for 3.14 alpha or beta versions

# Once installed, verify T-String support:
python3.14 -c "t = t'Test'; print(type(t))"

The official Python downloads page provides alpha and beta releases as they become available. Join the Python community discussions on python-discuss@python.org if you want to provide feedback on T-Strings and PEP 750.

Frequently Asked Questions

Are T-Strings backwards compatible with older Python versions?

No, T-Strings are a new feature in Python 3.14 and will not work in earlier versions. If you need to support older Python versions, you’ll need to either use f-strings or implement your own template processor. You can use `from __future__ import annotations` in Python 3.7+ to help with some compatibility, but the `t` prefix itself is new to 3.14.

What’s the performance impact of using T-Strings instead of f-strings?

T-Strings have a slightly higher memory footprint because they create Template objects rather than immediately evaluating to strings. However, if you’re processing templates (which is the entire point), the overhead is minimal compared to the safety benefits. For simple one-off templates where you don’t need processing, f-strings remain slightly more efficient.

Can I combine T-Strings with f-strings in the same code?

Absolutely! There’s no conflict between using both. Use f-strings for simple formatting and T-Strings when you need custom processing. In fact, many real applications will use both depending on context. Remember that you cannot use `f` and `t` prefixes together on the same string literal.

How do custom format specs work with T-Strings?

Format specs like `{value:.2f}` are captured in the Interpolation object’s `format_spec` attribute. Your custom processor can then apply these format specifications when processing the template. Here’s a quick example:

# format_spec_example.py
from __future__ import annotations

def format_aware_processor(template):
    parts = [template.strings[0]]

    for i, value in enumerate(template.values):
        interpolation = template.args[i]
        format_spec = interpolation.format_spec

        if format_spec:
            formatted = format(value, format_spec)
        else:
            formatted = str(value)

        parts.append(formatted)
        parts.append(template.strings[i + 1])

    return "".join(parts)

price = 19.5
quantity = 5
template = t"Price: ${price:.2f}, Qty: {quantity:03d}"
result = format_aware_processor(template)
print(result)

Output:

Price: $19.50, Qty: 005

Can T-Strings contain other T-Strings?

Yes, you can nest T-Strings, but the outer template will contain a Template object as one of its values rather than a string. You would need to process the inner template first, or create a processor that handles nested Template objects specially. Most use cases don’t require this complexity.

How do multiline T-Strings work?

T-Strings support multiline strings just like regular Python strings. Use triple quotes for multiline templates:

# multiline_template.py
from __future__ import annotations

name = "Bob"
email = "bob@example.com"

template = t"""
User Profile:
Name: {name}
Email: {email}
"""

print(template.strings)
print(template.values)

Output:

('"\n\nUser Profile:\nName: ', '\nEmail: ', '\n"')
('Bob', 'bob@example.com')

Conclusion

T-Strings represent a significant evolution in Python’s string handling capabilities, particularly for security-sensitive applications. By deferring the combination of string parts and values, they enable custom processing that’s impossible with f-strings, making your code more secure against injection attacks and more flexible for advanced use cases.

The key advantages are clear: T-Strings naturally support SQL injection prevention, HTML escaping, URL encoding, and custom domain-specific languages through a clean, consistent API. Whether you’re building web applications, CLI tools, or data processing pipelines, understanding and leveraging T-Strings will improve both the security and maintainability of your code.

For more information, consult the official Python documentation for PEP 750 at https://peps.python.org/pep-0750/ and the standard Template protocol documentation in the Python standard library.

Explore these related topics to deepen your Python expertise:

  • String Formatting in Python: A Complete Guide (f-strings, .format(), and legacy methods)
  • SQL Injection: Prevention Strategies and Best Practices
  • Web Security in Python: CSRF, XSS, and CORS
  • Building Custom DSLs with Python
  • Advanced Template Engines: Jinja2, Mako, and Cheetah