Why can't an f-string be exploited the way str.format with a user template can?
answer
- Ask when the interpolation is decided
- One of the two is grammar, not data
- The compiler fixes the expressions in advance
- A runtime string only bites in .format
- eval is how teams reintroduce the bug
basics
~10 sAn f-string is syntax: CPython compiles its interpolations from your source file, so a template that arrives at runtime can never become one. Only str.format, or eval, interprets a runtime string as a template.
solid answer
~50 sThe difference is *when* interpolation is decided. `f"Hello {user.name}"` is parsed by the compiler; the expressions that can ever run are fixed by the source file before any user input exists. `str.format` is a method call whose template is an ordinary `str` that may have arrived in a request body, and the format mini-language lets that string perform attribute and item access. So an attacker who supplies a template can only attack the runtime interpreter of templates. Two qualifiers show depth. First, the wrong way to give users f-string syntax is `eval` — that upgrades a read-only attribute walk into arbitrary code execution, because `eval` accepts calls and imports that the mini-language does not. Second, "safe" here means safe from template injection only; an f-string is still concatenation, so HTML, shell and query sinks need their own encoding.
code
python · 12 linesAPI_KEY = "sk-live-not-a-real-key"
class Order:
def __init__(self, ref):
self.ref = ref
payload = "{0.__init__.__globals__[API_KEY]}" # arrives in a request body
print(f"{payload}") # f-string: the text stays data
print(payload.format(Order("A-1"))) # str.format: the text is a templatego deeper
Be ready to say that f-string braces are compiled from your source file, so a string that arrives at runtime is never interpolated by one — it is only data being printed. Know that str.format is the call that interprets a runtime string.
Explain compile-time versus runtime interpolation precisely, and name what str.format's mini-language grants a template author: attribute and item access. Mention that PEP 701 in 3.12 changed the grammar, not the security property.
Show the failure mode you would catch in review: someone reaching for eval to give users f-string ergonomics. Be able to separate template injection from output encoding at the sink, and say which control belongs where.
Frame the rule the codebase should hold — templates are code and live in source; only values cross the trust boundary — and say how you would keep dynamic-template features from re-entering through localisation tables, admin UIs and plugin hooks.
### The difference is when the interpolation happens An f-string is **syntax**. When CPython compiles `f"Hello {user.name}"`, the compiler parses the braces at compile time and emits bytecode that evaluates `user.name` and formats it. The set of expressions that can ever run is fixed by the source file, and the fixing happens before your program has seen a single byte of user input. `str.format` is a **method call on a runtime string**. The template it interprets is an ordinary `str` object, which may have arrived seconds earlier in a request body. That difference — compile-time grammar versus runtime data — is the entire security story. An attacker who controls a template can only attack the mechanism that interprets templates at runtime. ```python payload = "{0.__init__.__globals__[API_KEY]}" # from a request print(f"{payload}") # prints the text: it is data print(payload.format(some_object)) # interprets the text: it is a template ``` The f-string does not "escape" the payload or defend against it. It simply never treats it as a template: the only interpolation the f-string performs is the one written between its own braces, on the variable `payload`, whose value is then converted with `str` and inserted verbatim. ### Why the question is asked It separates candidates who reason about *where the template comes from* from candidates who have learned "f-strings are the modern way to format." The second group reliably says either "f-strings are the same thing, so they must be equally vulnerable" or "f-strings are safe, use them everywhere" — and the second answer leads to the genuinely dangerous mistake described below. ### The trap: making user templates behave like f-strings Because f-string syntax is pleasant, teams that want customer-editable templates sometimes try to give users f-string semantics. The only way to do that with a runtime string is to compile it: `eval('f"' + template + '"')`, or `eval` of the expression, or `exec`. That converts a read-only attribute-walk into **arbitrary code execution** — `eval` accepts calls, imports and attribute assignment, none of which the format mini-language allows. Trading an information leak for RCE in the name of "using the safer formatting style" is the worst possible outcome, and an interviewer asking this question is often listening for exactly that. If users must supply templates, the answer is a restricted renderer: a placeholder syntax without attribute or item access, or a `string.Formatter` subclass that vets every field name against an allowlist and receives only pre-stringified values. ### What f-strings do *not* protect you from "Safe" here means one specific thing: safe from **template injection**, because the template cannot be attacker-controlled. It says nothing about what happens to the interpolated *value* at its destination. An f-string is string concatenation with nicer ergonomics, so building HTML, a shell command line, a log line or query text out of one puts raw user data into a context that has its own grammar. Those sinks each need their own encoding or structured API; formatting style is orthogonal to all of them. A candidate who answers "f-strings are safe" without naming what they are safe *from* has only half the concept. ### Version context f-strings landed in Python 3.6. PEP 701, in **3.12**, formalised their grammar in the parser: you may now reuse the same quote character inside the expression, use backslashes, nest arbitrarily deeply and write multi-line expressions with comments. That is a parsing change and it did not alter the security property — interpolation is still resolved at compile time from source, on 3.12, 3.13 and 3.14 alike. **3.14** adds template strings (t-strings, PEP 750). A `t"..."` literal is compile-time syntax exactly like an f-string, so it does not help with attacker-supplied templates either; what it changes is that the literal parts and the interpolated values stay separate until a renderer joins them, which is a *sink-escaping* mechanism rather than a template-injection one. ### The compact answer Say it in one line and then justify it: an f-string is compiled from your source, so its interpolations are chosen by you at compile time and cannot be supplied at runtime; `str.format` interprets a runtime string, so whoever supplies that string writes the expressions. Then add the two qualifiers that show depth — that `eval` is the wrong way to give users f-string syntax, and that "safe" here means safe from template injection, not from unescaped output at a sink.
- A colleague suggests eval('f"' + template + '"') so customers can use f-string syntax — what is wrong with it?It turns an information leak into remote code execution. The format mini-language allows only attribute and item reads; `eval` compiles arbitrary Python, so calls, imports, attribute assignment and comprehensions all become available to whoever wrote the template. The desire is legitimate — users want expressive templates — but the answer is a restricted renderer with an allowlist of field names, not a compiler.
- Did PEP 701 in Python 3.12 change any security property of f-strings?No. It moved f-string parsing into the main grammar so you may reuse the same quote character inside an expression, use backslashes, nest arbitrarily and write multi-line expressions with comments. Every one of those is a source-level ergonomics change. Interpolation is still resolved at compile time from the source file, which is the property that makes template injection impossible.
- Is an f-string safe to build HTML with?Not in any general sense. It is safe from template injection, because you wrote the template; it does nothing about the interpolated value's meaning at its destination. Inserting user text into markup with an f-string is plain concatenation, so the value needs escaping for that context — `html.escape` or a renderer that escapes by construction. Saying "f-strings are safe" without naming what they are safe from is half an answer.
saying these in an interview costs you the question
- Says an f-string is just a nicer .format() and equally exploitable
- Thinks an f-string can interpolate a template read at runtime
- Suggests eval to render user-supplied templates
- Calls f-strings safe without saying safe from what
- Claims PEP 701 in 3.12 changed f-string security