Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Python  Module 16 – Practical Python: text, CLIs, web, automation and security

Template strings (t-strings) in Python 3.14

Write t-strings in Python 3.14, read their Template and Interpolation parts, and build HTML and SQL helpers that keep user input from turning into code.

  • Intermediate
  • 20 minutes
  • Examples run with Python 3.14.8 and Pyodide 314.0.7
  • By MySmartCoPilot
Free core, Pro depth: 8 of 20 minutes free
Free: A t-string evaluates to a Template, not to text · A processing function escapes the values and keeps the text · f-strings, t-strings and string.Template differ in when values join the text · 2 more sections. With a pass: 2 in-depth parts and 3 interview answers, about 12 minutes.

What you will learn

  • Create t-strings and inspect Template and Interpolation objects
  • Write a safe HTML renderer that escapes interpolations
  • Explain how t-strings help prevent injection bugs
  • Convert a t-string into a sqlite3 query with placeholders and parameters
  • Predict how templates compare, join and evaluate their values

Before you start

A comment box receives <script>alert("hi")</script>, and a page that puts the comment into its HTML with an f-string sends that script to every visitor’s browser. No helper could have stopped it: by the time any code saw the text, the comment and the markup were one finished string.

A t-string, t"Hello {name}", keeps them apart. It evaluates to a string.templatelib.Template, an object that holds the text you wrote and each interpolated value separately, so a function can decide how to combine them: escape every value for HTML, or turn every value into an SQL parameter. Template strings arrived in Python 3.14 through PEP 750. Use them when a helper builds HTML, SQL, shell commands or log records from values; for text that only people read, f-strings stay the simpler choice.

A t-string evaluates to a Template, not to text

The parts of one t-string, printed one by one Python · parts.py
from string.templatelib import Interpolation

name = "Asha"
amount = 1234.5
greeting = t"Hi {name!r}, you owe {amount:,.2f} by Friday."

print(type(greeting))
print(greeting.strings)
print(greeting.values)

# Iterating gives the text you wrote and an Interpolation for every {...}, in order.
for part in greeting:
    if isinstance(part, Interpolation):
        print(f"  value={part.value!r} expression={part.expression!r} "
              f"conversion={part.conversion!r} format_spec={part.format_spec!r}")
    else:
        print(f"  text {part!r}")

# A Template has no text of its own: str() shows its repr.
print(str(greeting))

Output

<class 'string.templatelib.Template'>
('Hi ', ', you owe ', ' by Friday.')
('Asha', 1234.5)
  text 'Hi '
  value='Asha' expression='name' conversion='r' format_spec=''
  text ', you owe '
  value=1234.5 expression='amount' conversion=None format_spec=',.2f'
  text ' by Friday.'
Template(strings=('Hi ', ', you owe ', ' by Friday.'), interpolations=(Interpolation('Asha', 'name', 'r', ''), Interpolation(1234.5, 'amount', None, ',.2f')))

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 parts.py

The template keeps its pieces in three tuples: strings holds the text you wrote, always one item more than there are interpolations, interpolations holds an Interpolation for every pair of braces, and values holds the evaluated values. Step through the template that the program built:

Interactive · step by step The parts of one t-string Step through the Template that parts.py builds. The left column is the text written in the code, the right column the interpolations, in the order a loop over the template meets them.
The parts of one t-string, step 5 of 5. Showing 'Hi ', Interpolation (value 'Asha', conversion 'r'), ', you owe ', Interpolation (value 1234.5, spec ',.2f'), ' by Friday.'.'Hi 'strings[0]Interpolationvalue 'Asha', conversion 'r'', you owe 'strings[1]Interpolationvalue 1234.5, spec ',.2f'' by Friday.'strings[2]

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. The text you wrote. The t-string runs when its line runs, like an f-string, and gives a Template. It cuts the written text at every pair of braces and keeps the pieces in strings, three for two pairs of braces.
  2. The first value. The first pair of braces becomes an Interpolation: value 'Asha', expression 'name', the source text between the braces, and conversion 'r' from the !r. Its format_spec is empty.
  3. The second value. The second holds value 1234.5, expression 'amount' and format_spec ',.2f'. The spec was recorded, not applied, so the value is still the float 1234.5.
  4. The order of a loop. A for loop over the template meets the parts in their order on the page, text and interpolations taking turns, and skips any piece of text that is empty.
  5. No text of its own. values is the tuple ('Asha', 1234.5), and str() of the template shows its repr, because a Template turns into text only when a function decides how.

An interpolation carries four things: value, the result of the expression; expression, the source text between the braces; conversion, "r", "s", "a" or None; and format_spec, the text after the colon. Iterating the template gives the text and the interpolations, in order, and skips empty text.

Two details in the output matter. The conversion: !r, !s or !a after the expression in an f-string's braces: the value is turned into text with repr(), str() or ascii() before it is formatted. f"{city!r}" shows a string in quotes. More in “f-strings and the format mini-language” !r and the format specification: The code after the colon in a replacement field, such as >12,.2f, that sets fill and alignment, sign, width, grouping, precision and type. format(value, spec) applies one on its own. More in “f-strings and the format mini-language” ,.2f were recorded but not applied: the value is still the float 1234.5, and applying them, or ignoring them, is the processing function’s choice. And str() of the template printed its repr, not a greeting, because a Template has no text of its own: code that forgets to process a template shows Template(strings=… on screen instead of hiding the bug.

The prefix can be combined with r for a raw t-string, rt"\d{4}-{year}", where backslashes stay as written, but not with b: there are no bytes templates. Any expression an f-string: A string literal with f before its opening quote, such as f"{price:.2f}". The expressions in its braces are evaluated when the line runs, and their values are put into the text. More in “f-strings and the format mini-language” accepts works between the braces, and the expression attribute keeps its source text, which processors use as a name, for example for a log field.

A processing function escapes the values and keeps the text

One template, values escaped; the same text as an f-string Python · html_render.py
import html
from string.templatelib import Interpolation, Template


def render_html(template: Template) -> str:
    """Join a template, escaping every interpolated value for HTML."""
    out = []
    for part in template:
        if isinstance(part, Interpolation):
            out.append(html.escape(str(part.value)))   # a value: escape it
        else:
            out.append(part)                           # text written in the code: keep it
    return "".join(out)


user = "Ravi <admin>"
comment = '<script>alert("hi")</script> & thanks'

print(render_html(t"<p><b>{user}</b>: {comment}</p>"))
# An f-string has already joined everything, so nothing can tell the values apart any more.
print(f"<p><b>{user}</b>: {comment}</p>")

Output

<p><b>Ravi &lt;admin&gt;</b>: &lt;script&gt;alert(&quot;hi&quot;)&lt;/script&gt; &amp; thanks</p>
<p><b>Ravi <admin></b>: <script>alert("hi")</script> & thanks</p>

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 html_render.py

render_html() is a processing function, the piece that gives a template its meaning. It keeps the text written in the code, the <p> and <b> tags, and passes every value through html.escape(), so the user name and the comment arrive as visible characters instead of markup. The same loop, written at the top level so that every pass can be stepped through:

Interactive · line by line A processing loop that escapes values and keeps the text Step through the loop over the template's parts. Watch part, the piece the loop has taken, and out, the pieces of the page, growing one per pass.

Program · html_trace.py

from html import escape
from string.templatelib import Interpolation

user = "Ravi <admin>"
page = t"<p><b>{user}</b></p>"
out = []
for part in page:
    if isinstance(part, Interpolation):
        out.append(escape(part.value))
    else:
        out.append(part)
print("".join(out))
print(f"<p><b>{user}</b></p>")

Variables

After line 13: escape is escape(s, quote=True); Interpolation is Interpolation; user is 'Ravi <admin>'; page is Template(strings=('<p><b>', '</b></p>'), interpolations=(In…; out is ['<p><b>', 'Ravi &lt;admin&gt;', '</b></p>']; part is '</b></p>'.escapeescape(s, quote=True)InterpolationInterpolationuser'Ravi <admin>'pageTemplate(strings=('<p><b>', '</b></p>'), interpolations=(In…out'<p><b>'0'Ravi &lt;admin&gt;'1'</b></p>'2part'</b></p>'
  • changed in this step

Printed so far

<p><b>Ravi &lt;admin&gt;</b></p>
<p><b>Ravi <admin></b></p>

Every step was recorded from a run with Python 3.14.8 on macOS 26 arm64.

The drawing shows the variables when the program ends, and the program's 18 steps are listed below.

Line run 13
Next line end
Loop pass –
Every step as text
  1. Start. No line has run yet, so there are no variables. Python starts with line 1.
  2. Line 1. from html import escape loads escape.
  3. Line 2. from string.templatelib import Interpolation loads Interpolation.
  4. Line 4. user = "Ravi <admin>" sets user to 'Ravi <admin>'.
  5. Line 5. page = t"<p><b>{user}</b></p>" sets page to Template(strings=('<p><b>', '</b></p>'), interpolations=(In….
  6. Line 6. out = [] makes out a list of 0 items.
  7. Line 7. for part in page: starts: part is '<p><b>'. A loop over a template gives its parts in order. The first is text written in the code, '<p><b>'.
  8. Line 8. isinstance(part, Interpolation) is false (part is '<p><b>'), so the else block runs.
  9. Line 11. out.append(part) adds '<p><b>' to the end of out. Key point Text written in the code is markup the programmer meant, so the else branch keeps it as it is.
  10. Line 7. for part in page: goes round again: part is Interpolation('Ravi <admin>', 'user', None, '').
  11. Line 8. isinstance(part, Interpolation) is true (part is Interpolation('Ravi <admin>', 'user', None, '')), so the block under if isinstance(part, Interpolation): runs.
  12. Line 9. out.append(escape(part.value)) adds 'Ravi &lt;admin&gt;' to the end of out. The fix The value came from a user, so it is escaped: < and > become &lt; and &gt;, and the browser shows them as characters instead of reading a tag.
  13. Line 7. for part in page: goes round again: part is '</b></p>'.
  14. Line 8. isinstance(part, Interpolation) is false (part is '</b></p>'), so the else block runs.
  15. Line 11. out.append(part) adds '</b></p>' to the end of out.
  16. Line 7. for part in page: has no item left, so the loop ends.
  17. Line 12. print("".join(out)) prints <p><b>Ravi &lt;admin&gt;</b></p>. The page has its tags, and the user name appears as text.
  18. Line 13. print(f"<p><b>{user}</b></p>") prints <p><b>Ravi <admin></b></p>. That was the last line, so the program ends. The bug The f-string pasted the value into the text before anything could look at it, so its <admin> reaches the browser as a tag.

The f-string on the last line had no such chance: by the time any function received it, the <script> from the comment was part of one finished string, indistinguishable from the tags the programmer wrote. That separation is the whole idea, and PEP 750 names injection attacks, such as cross-site scripting: A weakness in which text from one user reaches a page without escaping, so the browser reads it as markup and can run code in other visitors' browsers. Escaping output for its place in the page prevents it. More in “XML and HTML parsing without security holes”, among its reasons. Escaping still has to suit the place where the value lands: html.escape() protects text between tags and quoted attribute values, but a link in href also needs its scheme checked, and code inside <script> needs a different encoder altogether.

Key idea

A t-string hands its processing function the written text and the values apart, so the function can trust the first and escape the second; an f-string has already joined them.

Checkpoint

Check: The parts of a Template · Escaping in a processing function

  1. Question 1 of 2 With a, b, c = 1, 2, 3, how many items does t"{a} and {b}{c}".strings hold?

    Type a number.

    A hint

    strings always holds one item more than there are interpolations, even where the text between two braces is empty.

    Show the answer: Question 1 of 2

    Answer: 4 strings

    Three interpolations mean four strings: '' before {a}, ' and ' between {a} and {b}, '' between {b} and {c}, and '' after {c}. Iterating the template skips the empty ones, but strings keeps them.

  2. Question 2 of 2 Why could the page built with the f-string not be repaired by calling html.escape() on its result?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The finished string mixes the tags you wrote with the values, so escaping it would escape the <p> and <b> tags too

    Escaping must happen to each value before it joins the text. A t-string keeps the values apart until the processing function joins them, so the function can escape the values and keep the tags.

f-strings, t-strings and string.Template differ in when values join the text

All three put values into text. Read the table row by row for when the values join the written text, and who decides how:

Three ways to put values into text
Criterion f-stringt-stringstring.Template
Written as f"Hi {name}" in codet"Hi {name}" in codeTemplate("Hi $name"), often from a file
Evaluates to A finished strA Template objectA Template, filled later
Values are Formatted and inserted at onceComputed at once, combined by a functionLooked up by name when filled
Escaping None, the text is finalWhatever the function doesNone
When to choose Messages people readHTML, SQL, logs and shell built by helpersText that people edit

Ask who combines the values

If nobody needs to treat the values differently, an f-string is enough. If a helper must escape or bind them, write a t-string. If someone else writes the text, fill a string.Template.

A processor can also format, recurse, bind and log

A t-string becomes a Template holding the text and the values apart; html(), sql() or log_record() then combine them.t"<b>{user}</b>"written in your codeTemplatestrings: ("<b>", "</b>")interpolations: (user,)A processing function decideshtml(): escape each value<b>Ravi &lt;admin&gt;</b>sql(): a ? per valuequery text + parameterslog_record(): message+ fields for searchevaluated at once

From a t-string to HTML, SQL or a log record

Text description of the diagram

The diagram has three steps, one under the other.

  1. The t-string t"<b>{user}</b>" is written in the code.
  2. It is evaluated at once into a Template, which keeps the written text, "<b>" and "</b>", apart from the interpolated value, user.
  3. A processing function decides how to combine them. html() escapes each value, so the user name Ravi <admin> appears as Ravi &lt;admin&gt; between the bold tags. sql() puts a question mark in place of each value and passes the values as parameters. log_record() builds a message for people and keeps the values as separate fields for search.

A real processor usually does more than escape. string.templatelib.convert() applies an interpolation’s !r, !s or !a conversion the way an f-string would. format(value, spec) applies its format spec, such as ,.2f. A value that is itself a template, such as a list item built with t"<li>{item}</li>", can be rendered recursively. An SQL processor writes a ? placeholder (SQL): A marker such as ? or :name where a value goes in an SQL statement. The driver sends the values apart from the SQL text, and the database binds them to the compiled statement as data. More in “SQLite databases with sqlite3” for every value and returns the values as parameters. A logging processor returns a readable message together with the values as separate fields. Templates can also be joined with +, but only to other templates.

Version note

t-strings need Python 3.14 or later. On Python 3.13 a file that contains one is a SyntaxError before any of it runs, so libraries that support older versions cannot use them yet. The standard library’s sqlite3 does not accept templates either, and raises TypeError for one: keep passing its ? placeholders and parameters, or convert a template into that pair yourself.

Checkpoint

Check: What else a processor does

  1. Question 1 of 2 What does the standard library's sqlite3 do when you pass it a Template as the query?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: It raises TypeError, so you convert the template into a query with ? placeholders and parameters yourself

    Keep passing sqlite3 its query with ? placeholders and a tuple of parameters, or write a processor that turns a template into exactly that pair.

  2. Question 2 of 2 With x = 5, which expression is allowed?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: t"total: " + t"{x}"

    Templates can be joined with +, but only to other templates. Write the extra text as a t-string too, and the processor keeps treating written text and values differently.

Check yourself

Eight questions on the three ideas above follow, one at a time, in the order your answers call for.

Practice that adapts to you

Practise this lesson

8 questions about 9 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 8 What does this print on Python 3.14?

    Read the code, then choose one answer.

    name = "Asha"
    print(type(t"Hi {name}").__name__, type(f"Hi {name}").__name__)
    Show the answer: Question 1 of 8

    Answer: Template str

    A t-string evaluates to a string.templatelib.Template, while an f-string evaluates to a str in which the value has already been inserted.

  2. Question 2 of 8 What does this print?

    Read the code, then choose one answer.

    city = "Pune"
    parts = list(t"Ship to {city}!")
    print(len(parts), parts[1].value, parts[1].expression)
    Show the answer: Question 2 of 8

    Answer: 3 Pune city

    Iterating the template gives the text "Ship to ", an Interpolation and the text "!", three parts. The interpolation's value is the evaluated value and its expression is the text written between the braces.

  3. Question 3 of 8 A function expects a str, and someone passes it t"Total: {total}" by mistake. What does str() of the template give?

    Choose one answer.

    Show the answer: Question 3 of 8

    Answer: Its repr, starting with Template(strings=…, which makes the mistake visible

    A Template has no text rendering of its own, so str() falls back to its repr. Printing it shows Template(strings=('Total: ', ''), interpolations=(…)) instead of a total, so the bug is easy to spot.

  4. Question 4 of 8 Why can an HTML helper built on t-strings stop injected markup when one built on f-strings cannot?

    Choose one answer.

    Show the answer: Question 4 of 8

    Answer: It receives each value separately from the written text, so it can escape the values and keep the markup

    A t-string does no escaping on its own. It keeps the values apart from the text you wrote, so the processing function can treat them differently; an f-string hands over one finished string in which they are mixed.

  5. Question 5 of 8 With render_html() from the example, what does this print?

    Read the code, then choose one answer.

    x = "a & b"
    print(render_html(t"<i>{x}</i>"))
    Show the answer: Question 5 of 8

    Answer: <i>a &amp; b</i>

    The processing function keeps each piece of written text and escapes each value, so only the user's part changes.

  6. Question 6 of 8 A processor escapes every value with html.escape(). A user's profile link is placed in href="{url}". What can still go wrong?

    Choose one answer.

    Show the answer: Question 6 of 8

    Answer: A link whose scheme is javascript: survives escaping, so the processor must also check the scheme

    Escape for the place a value lands. Text between tags and quoted attributes need html.escape(); a link also needs an allowed scheme such as https, and code inside <script> needs a different encoder.

  7. Question 7 of 8 Which function applies an interpolation's !r, !s or !a the way an f-string would?

    Choose one answer.

    Show the answer: Question 7 of 8

    Answer: string.templatelib.convert()

    An interpolation records its conversion without applying it. convert() applies it, and format() then applies the format spec.

  8. Question 8 of 8 What should an SQL processor return for t"SELECT * FROM marks WHERE name = {name}"?

    Choose one answer.

    Show the answer: Question 8 of 8

    Answer: The query "SELECT * FROM marks WHERE name = ?" and the parameters (name,)

    An SQL processor writes a ? placeholder for every value and returns the values as parameters, so the database receives the text and the data separately.

Processors for SQL, logs and nested HTML, with an injection attempt run Pro: opens with a Pro, Premium or Ultimate pass

A processing function is where a template’s safety is won or lost. Here an injection attempt runs against an f-string query and a t-string query, a quoting mistake is caught, one template becomes a log message and its fields, and one renderer handles conversions, format specs and nested templates.

In this part:

  • An injection attempt, run against an f-string and a t-string
  • One template, two results: a message and searchable fields
  • Conversions, format specs and nested templates in one renderer
  • Exercise: an HTML renderer with a safe marker, and SQL with placeholders
    • Render t-strings as escaped HTML and as SQL with placeholders

1 diagram · 3 runnable examples (Python) · 1 exercise with sample tests · about 9 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

How templates compare, join and evaluate, with their cost measured Pro: opens with a Pro, Premium or Ultimate pass

Templates behave differently from strings in seven ways that surprise people, and processing them costs time. Each behaviour is run below, the cost is measured against an f-string, and the adoption advice that follows rests on both.

In this part:

  • Seven behaviours that surprise people, each run
  • What a t-string costs, measured against an f-string
  • Caching work by the template’s strings
  • When to use t-strings now, and what to keep

2 runnable examples (Python) · about 7 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Interview questions

  1. Fresher to mid · Warm-up

    What does a t-string evaluate to in Python 3.14, and how is it different from an f-string?

    An f-string evaluates to a finished str: the values are formatted and inserted at once, so nothing afterwards can tell the text you wrote from the values. A t-string, written t"Hi {name}", evaluates to a string.templatelib.Template instead. The template keeps the written text in its strings attribute and each interpolated value as an Interpolation with value, expression, conversion and format_spec, and conversions and format specs are recorded rather than applied. A function then decides how to combine the parts: it can escape every value for HTML, turn values into SQL parameters, or keep them as fields of a log record. The values are still computed when the t-string is created. A Template has no text of its own, so str() shows its repr. t-strings need Python 3.14 or later.

  2. Mid to senior

    A helper run_query(sql: str) is called as run_query(f"SELECT * FROM users WHERE email = '{email}'") all over a codebase that now runs on Python 3.14. Explain the risk, and design a replacement based on t-strings, including quoted placeholders, lists of values and table names.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  3. Mid to senior

    Design a small HTML library for a dashboard in which all markup is built with t-strings. Which parts does it escape and which does it trust, how do nested components and attributes work, and how do you stop developers from bypassing the escaping by accident?

    Model answer, Pro. First step and follow-ups

  4. Senior to staff

    Your team maintains a library that supports Python 3.10 to 3.14, and someone asks to switch to t-strings everywhere. What do you advise, what would you measure, and how would you introduce t-strings without breaking users on older versions?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • t"..." evaluates to a string.templatelib.Template, not a str: its strings hold the written text, and its interpolations hold each value, expression, conversion and format_spec.
  • Conversions and format specs are recorded, not applied, and str() of a template shows its repr.
  • A processing function gives a template its meaning: it escapes the values for HTML, binds them for SQL or keeps them as fields for logs, while the written text stays as it is.
  • t-strings need Python 3.14, and sqlite3 still takes its ? placeholders and parameters.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.