PEP-750 introduces template strings (t-strings) in Python 3.14, a powerful generalization of f-strings that helps developers write more secure code. Instead of evaluating to str, t-strings evaluate to a new type, Template:
template: Template = t"Hello {name}"Python f-strings are easy to use and very popular. However, they provide no way to intercept and transform interpolated values before they are combined into a final string. This limitation makes them unsuitable for certain security-sensitive use cases.
As a result, incautious use of f-strings can lead to serious security vulnerabilities:
- SQL Injection Attacks: When developers use f-strings to build SQL queries
- Cross-Site Scripting (XSS): When unescaped user input is included in HTML output
Template strings address these problems by providing developers with access to the string and its interpolated values before they're combined, enabling proper sanitization and escaping.
SQL injection occurs when untrusted data is inserted directly into a SQL query string without proper sanitization. Using f-strings to build SQL queries is particularly dangerous:
# DANGEROUS - DO NOT USE! Vulnerable to SQL injection
username = "user'; DROP TABLE users; --"
query = f"SELECT * FROM users WHERE username = '{username}'"
cursor.execute(query) # Could delete your users table!With template strings, we can create a safer SQL query builder:
import sqlite3
from string.templatelib import Template, Interpolation
def safe_sql(template: Template) -> tuple[str, list]:
query_parts = []
params = []
for item in template:
match item:
case str() as s:
query_parts.append(s)
case Interpolation() as interp:
query_parts.append("?") # Use parameterized query
params.append(interp.value)
return "".join(query_parts), params
# Safe usage
username = "user'; DROP TABLE users; --"
template = t"SELECT * FROM users WHERE username = '{username}'"
query, params = safe_sql(template)
cursor.execute(query, params) # Safe, uses parameters
# This produces:
# query = "SELECT * FROM users WHERE username = ?"
# params = ["user'; DROP TABLE users; --"]The safe_sql function extracts the interpolated values and uses them as parameters in a prepared statement, which is the recommended way to prevent SQL injection.
Cross-Site Scripting (XSS) occurs when untrusted data is included in a web page without proper HTML escaping:
# DANGEROUS - DO NOT USE! Vulnerable to XSS
user_input = "<script>stealCookies()</script>"
html = f"<div>Welcome, {user_input}!</div>" # Executes malicious JavaScriptWith template strings, we can create an HTML template processor that automatically escapes content:
import html as html_escape
from string.templatelib import Template, Interpolation
def html(template: Template) -> str:
parts = []
for item in template:
match item:
case str() as s:
parts.append(s)
case Interpolation() as interp:
if isinstance(interp.value, dict) and interp.expression.strip() == "attributes":
# Special handling for HTML attributes
attr_str = " ".join(f'{k}="{html_escape.escape(str(v))}"'
for k, v in interp.value.items())
parts.append(attr_str)
else:
# Auto-escape all other interpolated values
parts.append(html_escape.escape(str(interp.value)))
return "".join(parts)
# Safe usage - content is automatically escaped
evil_input = "<script>alert('evil')</script>"
template = t"<div>Welcome, {evil_input}!</div>"
safe_html = html(template)
# Result: "<div>Welcome, <script>alert('evil')</script>!</div>"
# Handle HTML attributes elegantly
attributes = {"class": "user-profile", "data-id": "12<>"}
template = t"<div {attributes}>User content</div>"
safe_html = html(template)
# Result: '<div class="user-profile" data-id="12<>">User content</div>'This approach ensures that all user-provided content is automatically escaped, preventing XSS vulnerabilities while making the code clean and readable.
It is easy to "implement" f-strings using t-strings. That is, we can write a function f(template: Template) -> str that processes a Template in much the same way as an f-string literal, returning the same result:
name = "World"
value = 42
templated = t"Hello {name!r}, value: {value:.2f}"
formatted = f"Hello {name!r}, value: {value:.2f}"
assert f(templated) == formattedThe f() function supports both conversion specifiers like !r and format specifiers like :.2f. The full code is fairly simple:
from string.templatelib import Template, Interpolation
from typing import Literal
def convert(value: object, conversion: Literal["a", "r", "s"] | None) -> object:
if conversion == "a":
return ascii(value)
elif conversion == "r":
return repr(value)
elif conversion == "s":
return str(value)
return value
def f(template: Template) -> str:
parts = []
for item in template:
match item:
case str() as s:
parts.append(s)
case Interpolation(value, _, conversion, format_spec):
value = convert(value, conversion)
value = format(value, format_spec)
parts.append(value)
return "".join(parts)Structured logging allows developers to log data in machine-readable formats like JSON. With t-strings, developers can easily log structured data alongside human-readable messages using just a single log statement.
import json
import logging
from string.templatelib import Interpolation, Template
from typing import Mapping
class TemplateMessage:
def __init__(self, template: Template) -> None:
self.template = template
@property
def message(self) -> str:
# Use the f() function from the previous example
return f(self.template)
@property
def values(self) -> Mapping[str, object]:
return {
item.expression: item.value
for item in self.template
if isinstance(item, Interpolation)
}
def __str__(self) -> str:
return f"{self.message} >>> {json.dumps(self.values)}"
# Usage example
_ = TemplateMessage # optional, to improve readability
action, amount, item = "traded", 42, "shrubs"
logging.info(_(t"User {action}: {amount:.2f} {item}"))
# Outputs:
# User traded: 42.00 shrubs >>> {"action": "traded", "amount": 42, "item": "shrubs"}Template strings (PEP-750) provide a powerful mechanism for making Python applications more secure by allowing developers to intercept and transform interpolated values before they are combined into a final string. This is particularly valuable for preventing common security vulnerabilities like SQL injection and XSS attacks.
By using template strings instead of f-strings in security-sensitive contexts, developers can write code that is both clean and secure, helping to prevent some of the most common and dangerous security vulnerabilities in web applications.
