From % to F-Strings to T-Strings: A Tale of Python’s String Interpolation Journey

Friday, 10 October 2025
Python

Python 3.14 (π-thon) has officially been released, bringing a wealth of new features and improvements. Among them is a new templating syntax for strings, marking the next step in Python’s long journey from interpolation to structured templating. This milestone offers a perfect opportunity to look back over Python’s 30-year history and explore how string formatting has evolved — from the early % operator to the modern F-Strings, and now to the all-new T-Strings.

The Stone Age: C-Style (%) Formatting

In the early days of Python, C-style string formatting was the only option. It felt straightforward — especially if you had a background in C’s printf — but it wasn’t type-safe.

name = "Alice"
age = 30
result = "Name: %s, Age: %d" % (name, age)
print(result)

You could use:

  • %s for strings
  • %d for integers
  • %f for floating-point numbers, and so on.

This approach maps closely to C’s formatting syntax. However, it’s legacy today: while it still works as of 2025, it’s considered outdated and is no longer recommended for new Python code.

The Next Step: str.format()

Python 2.6 / 3.0 introduced the next milestone in string formatting: the str.format() method. It was more powerful and flexible than the old % operator and became the go-to way of formatting strings for many years.

name = "Alice"
age = 30
result = "Name: {}, Age: {}".format(name, age)
print(result)
# → Name: Alice, Age: 30

The new method supported:

  • Alignment ({:<10}, {:>10})
  • Width and padding
  • Precision for floats ({:.2f})
  • Custom formatting through format specifiers.
pi = 3.14159
print("Pi rounded to two decimals: {:.2f}".format(pi))
# → Pi rounded to two decimals: 3.14

For some developers, str.format() felt slightly verbose, especially compared to f-strings that came later. Also, it does not sanitize values, so if you insert untrusted input directly (for example, in SQL queries), you’re still vulnerable to injection attacks.

F-Strings: Simpler, Faster, More Readable and Pythonic

Python 3.6 introduced F-strings (formatted string literals), which quickly became the preferred way to format strings. They’re concise, readable, and even faster than both % formatting and str.format() because the expressions are evaluated at runtime without extra function calls.

name = "Alice"
age = 30
result = f"Name: {name}, Age: {age}"
print(result)
# → Name: Alice, Age: 30

With F-Strings, you can embed any valid Python expression inside the braces:

print(f"In 5 years, {name} will be {age + 5}.")
# → In 5 years, Alice will be 35

Formatting options are also straightforward:

pi = 3.14159
print(f"Pi rounded to two decimals: {pi:.2f}")
# → Pi rounded to two decimals: 3.14

Thanks to their clarity and performance, F-strings have become the Pythonic way of doing string interpolation in modern Python code.

Template Strings from the string Module

Now let's switch to templating capabilities in Python. It’s worth noting that Python has long offered a templating approach to string substitution. Unlike %, str.format(), or F-strings — which evaluate Python expressions directly — templates work via simple placeholder substitution.

This makes them safer for untrusted input, because no expressions are executed. Additionally, the template structure remains reusable, allowing you to substitute different values multiple times without redefining the template.

from string import Template

t = Template("Name: $name, Age: $age")
result = t.substitute(name="Alice", age=30)
print(result)
# → Name: Alice, Age: 30

result = t.substitute(name="Bob", age=35)
print(result)
# → Name: Bob, Age: 35

Key differences from formatters:

  • ✅ Safer for external input (no code execution)
  • ✅ Familiar $placeholder syntax
  • ❌ Limited formatting (no arithmetic, no type specifiers, no width/precision)
  • ❌ A bit more verbose

Template strings have been around since Python 2.4. They never became the default way of doing interpolation but remain useful for security-sensitive situations, such as generating SQL statements, HTML, or config files from external input.

T-Strings: Template Strings Reimagined in Python 3.14

Python 3.14 introduces a new templating mechanism for strings through PEP 750. These new strings are called template strings, or T-Strings for short.

A T-String is created by prefixing a string literal with t (e.g., t"Hello, {name}"). Unlike F-Strings, which evaluate expressions immediately and return a string, T-Strings return a Template object. This object contains both the static parts of the string and the dynamic interpolations as separate components, allowing for more controlled and secure processing.

name = "Green Tea"
price = 3.5
template = t"The product info => Name: {name}, Price: {price:.2f}"
print(type(template))
# → <class 'string.templatelib.Template'>

# Access the static string parts
print(template.strings)
# → ('The product info => Name: ', ', Price: ', '')

# Access the assigned values
print(template.values)
# → ('Green Tea', 3.5)

# Access the interpolation slots
print(template.interpolations)
# → (Interpolation('Green Tea', 'name', None, ''), Interpolation(3.5, 'price', None, '.2f'))

This structured approach enables developers to inspect and manipulate the components of the string before it is rendered. It is particularly useful for tasks like sanitizing user input in SQL queries or escaping HTML content to prevent injection attacks.

T-Strings are designed to address security concerns associated with F-Strings, particularly when handling untrusted input. By separating the static and dynamic parts of a string, T-Strings allow for safer handling of interpolated values. For instance, they can facilitate the sanitization of inputs to prevent SQL injection or cross-site scripting (XSS) vulnerabilities.

Based on PEP 750, developers can write arbitrary code to process template strings. The following two functions render T-Strings into regular strings: the first omits conversions and formatting, while the second applies them.

from string.templatelib import Template, convert
from typing import List


def render(template: Template) -> str:
    parts: List[str] = []

    for i in template:
        if isinstance(i, str):
            parts.append(i)
        else:   # if isinstance(i, Interpolation):
            parts.append(str(i.value))

    return "".join(parts)


def render_with_conversion(template: Template) -> str:
    parts: List[str] = []

    for i in template:
        if isinstance(i, str):
            parts.append(i)
        else:
            tmp = i.value
            if i.conversion:
                tmp = convert(i.value, i.conversion)
            if i.format_spec:
                tmp = format(tmp, i.format_spec)
            parts.append(tmp if isinstance(tmp, str) else str(tmp))

    return "".join(parts)


name = "Green Tea"
price = 3.5
template = t"The product info => Name: {name}, Price: {price:.2f}"

print(render(template))
# → The product info => Name: Green Tea, Price: 3.5

print(render_with_conversion(template))
# → The product info => Name: Green Tea, Price: 3.50

Looking back, it’s fascinating to see how Python’s approach to strings has grown alongside the language itself. Each change reminds us that even the smallest parts of the language can keep evolving — gracefully, and with purpose.

Top