Prompt and context
Your team is upgrading to Python 3.14 and wants to inspect interpolations before joining text. Explain what a t-string produces, how to traverse it, and why the template API is not automatic escaping.
PEP 750 adds template strings to Python 3.14. t"..." does not directly produce str; it produces string.templatelib.Template, retaining static segments and interpolation objects so an application can validate, escape, or transform each value before joining.
What the interviewer is testing
Cover the difference between f-string immediate stringification and a structured t-string, the fields of Template and Interpolation, raw values and format specs, context-specific escaping, side-effect control, and compatibility with older Python versions.
30-second answer framework
“I would start by saying that a t-string is not safe output by itself. It preserves static segments and interpolation objects so a renderer can inspect the raw value, expression text, conversion, and format spec, then apply SQL-, HTML-, or logging-specific encoding. I would restrict value types, test nested templates, format specs, exceptions, and old-version fallback, and validate the final output again at its destination.”
Step-by-step deep dive
Step 1: Contrast f-strings and t-strings
An f-string evaluates and formats to str, so its intermediate structure is gone. A t-string uses the t prefix and produces a Template; static text and interpolations remain separate until a renderer joins them.
Step 2: Understand the core objects
A template contains string segments and Interpolation objects. An interpolation records its value, expression text, conversion marker, and format spec, allowing policy to consider source and destination context. Iteration does not mean the result is already safe.
Step 3: Write a minimal processor
The following processor only demonstrates structural traversal; a real system still needs context-specific encoding:
from string.templatelib import Template
def render_plain(template: Template) -> str:
parts = []
for item in template:
if isinstance(item, str):
parts.append(item)
else:
parts.append(str(item.value))
return "".join(parts)
message = t"Hello, {name}!"str(item.value) is suitable only for a plain-text example. SQL, HTML, Shell, and logs need their own parameterization or encoding APIs.
Step 4: Handle conversions and format specs
An interpolation may carry !s, !r, or a format spec. The renderer should decide whether conversion is allowed, then encode the converted value for the destination context. repr is not HTML escaping, and a format spec must not bypass a type allowlist.
Step 5: Avoid side-effect expressions
A t-string still evaluates interpolation expressions to create Interpolation objects. The processor is not a sandbox; an untrusted template is already risky at construction time. For plugins or user input, accept data placeholders rather than dynamically executed expressions.
Step 6: Choose policy by output context
Plain text can use an allowlisted conversion; HTML needs an audited HTML escaper; SQL needs driver parameter binding; Shell should avoid concatenation and use an argument array; structured logs should carry fields instead of one joined line. Template supplies structure, not these security boundaries.
Step 7: Support versions and discover the API
Python 3.14 provides string.templatelib. A compatibility layer should select an existing template library or explicitly fall back to f-strings on older versions; it must not pretend to preserve interpolation structure there. Check the runtime version at startup and run CI on every supported interpreter.
Step 8: Test and observe
Test static segments, nested t-strings, format specs, exceptions, __format__ side effects, and large templates. Record rejected value types and template sources without logging sensitive interpolation content; run context-specific security regressions on the output.
Trade-offs and boundaries
Structured processing versus simple joining
Structure helps when a boundary needs auditing, localization, or multiple output targets. A one-off plain-text script is simpler with an f-string. Choose by risk and reuse rather than putting every string through a template pipeline.
Flexible formatting versus predictable types
Allowing arbitrary __format__ is expressive but introduces side effects and type drift. A shared renderer should restrict types, conversions, and format specs and return diagnosable errors for rejected values.
Template object versus final string
A Template is useful inside a controlled boundary. The final output still needs destination-specific validation before sending, storing, or executing it; it is not an already encoded security token.
Failure drills and evolution plan
HTML escaping is bypassed
Give an interpolation a custom __str__ returning markup and verify that plain str conversion is not safe. Replace it with context encoding and add malicious-value regressions.
A format spec raises
Use an unsupported spec and an object whose __format__ raises. The processor should identify the interpolation and reject the render instead of returning partial output.
An old interpreter runs the module
Start the same module on Python 3.13. Version checking should produce a clear error or choose an explicit compatibility implementation rather than deferring a syntax failure to production.
Common mistakes and follow-ups
Mistake 1: Assuming t-strings escape automatically
Follow-up: What do they solve? They preserve structure so policy can run before joining; SQL and HTML safety still belongs to dedicated APIs.
Mistake 2: Assuming interpolation expressions never run
Follow-up: When are they evaluated? They run while constructing the t-string, so an untrusted template does not gain sandboxing from this syntax.
Mistake 3: Treating repr as safe encoding
Follow-up: Why not? repr is a debugging representation and does not guarantee HTML, SQL, Shell, or log-field encoding rules.
Extended follow-ups and model answers
Should t-strings replace every f-string?
No. The structure is useful only at boundaries that inspect or transform interpolations before joining; ordinary plain text can remain an f-string.
How would you build a SQL renderer?
Do not generate a SQL string. Map static segments and values to driver placeholders, bind values through the driver, and use the template only to describe the statement structure.
How do you support older Python versions?
Check the runtime, use string.templatelib on 3.14, and use an existing implementation or an explicit unsupported error on older versions. Verify semantic differences with the same security test suite.