How do I use string.Template for safe substitution?¶
You need to drop user-supplied values into a string — an email body, a notification, a generated message — and you want the substitution to be safe even if the user data is wrong, missing, or hostile. f-strings and str.format() give you full Python expression power, which is exactly what you don't want when the template comes from outside your code.
string.Template is the deliberately-limited tool for this job: $name-style placeholders, no expressions, no attribute access, no method calls. You hand it a dict of values and it fills them in.
from string import Template
# A template — note this could safely be loaded from a file or database.
email = Template(
"Hi $name,\n\n"
"Your order #$order_id is ready for collection at $store.\n\n"
"Thanks,\nThe team"
)
# safe_substitute() leaves any missing placeholders as $name, rather than
# raising. That's almost always what you want for templates that come
# from outside your code.
message = email.safe_substitute(
name="Alice",
order_id="A1729",
store="Manchester Piccadilly",
)
print(message)
Two extras: choosing between substitute and safe_substitute, and a sketch of why templates are safer than f-strings for user-supplied templates.
from string import Template
t = Template("Dear $title $surname, your order #$order_id is ready.")
# substitute() is strict — KeyError if anything is missing
print(t.substitute(title="Dr", surname="Singh", order_id="A1729"))
try:
t.substitute(title="Dr", surname="Singh") # missing order_id
except KeyError as exc:
print(f"strict mode raised: {exc}")
# safe_substitute() leaves the placeholder in place — useful when
# you're building a message in stages, or when missing values should
# show through rather than crash.
print(t.safe_substitute(title="Dr", surname="Singh"))
# Dear Dr Singh, your order #$order_id is ready.
# Why templates beat f-strings for *user-supplied* templates
from string import Template
# Imagine this template came from a database row or a settings file —
# i.e. somewhere a user could edit it.
user_supplied = "Hi $name, your balance is $balance."
# string.Template only does name substitution. There's no way for the
# template author to read attributes, call methods, or evaluate expressions.
print(Template(user_supplied).substitute(name="Alice", balance="£42"))
# An f-string version would require eval() or a similar mechanism, which
# would let the template author run arbitrary Python — including reading
# secrets out of the surrounding scope. Don't do this:
#
# eval(f"f'{user_supplied}'")
#
# Use string.Template, or a real templating library like Jinja2 with
# autoescape and a sandboxed environment.
Why it works¶
string.Template solves a narrower problem than f-strings or str.format(), and that narrowness is the whole point.
The template syntax is just $name (or ${name} when you need to butt the placeholder up against following characters). There's no {obj.attr}, no {obj.method()}, no format spec, no expressions. The substitution mechanism takes a dict (or kwargs) of name → value and replaces each placeholder with the corresponding value's str(). That's it.
substitute() is the strict variant: any placeholder without a value, and any value without a placeholder, raises KeyError. Use it when the template and the data come from the same trust domain — typically your own code — and a mismatch is a programming error you want to find loudly.
safe_substitute() is the forgiving variant: missing placeholders are left in place as the literal $name text. Use it when the template is data — loaded from a config file, a database, a CMS — and you want a bad template to render imperfectly rather than crash the whole request.
The reason this matters for security is that f-strings and str.format() give the template author access to Python's expression syntax. f"{user.password}" or "{0.__class__.__init__.__globals__}".format(obj) can leak information from the surrounding scope. string.Template literally cannot do either — there's no syntax for it.
Trade-offs¶
Template is the right choice when the template comes from outside your trust boundary — user-editable settings, content authored by non-developers, anything loaded from a database. It's deliberately featureless; that's its security model.
It is the wrong choice when you control the template and want the expressiveness of full Python. For interpolating into a SQL string you're about to execute, an HTML page you're rendering, or a one-off log message, an f-string is shorter, faster, and easier to read. f-strings also win on performance — Template.substitute does work at runtime that an f-string does at compile time.
For real templating — loops, conditionals, includes, autoescaping — use Jinja2. string.Template is for simple value substitution; the moment you want {% if %} or {% for %}, you've outgrown it.
If you're tempted to subclass Template to change the delimiter (say, to %name% for legacy compatibility), it works, but think hard about whether the legacy format is worth supporting at all.
Related¶
- How to avoid common string mistakes — including the comparison between f-strings,
format, and templates. - String formatting reference — the full f-string and
format()mini-language for the cases where templates aren't the right tool. - How to clean and normalise text — useful before substituting user-supplied values into a template.