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.
Related Articles
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