string.Template: Safe and Simple Text Templates

Published on: September 9, 2026
Reading time: 6 minutes
Software developer creating text templates with Python string.Template

string.Template is a lightweight way to build text with replaceable fields in Python. Instead of embedding complex logic inside a string, you define placeholders such as $name and provide their values separately. This approach works well for notifications, email bodies, reports, configuration-driven messages, and text edited by people who should not need to understand full Python expression syntax.

What string.Template is

The Template class belongs to Python’s standard string module. It recognizes placeholders beginning with a dollar sign. A field can be written as $name or ${name}. Braces are useful when the identifier touches other characters.

from string import Template
model = Template("Hello, $name! Your plan is ${plan}Premium.")
text = model.substitute(name="Ana", plan="Python ")
print(text)

The final text is produced without manual concatenation. This reduces punctuation mistakes and keeps the message readable. For broader background, see Python strings and Python f-strings.

substitute versus safe_substitute

substitute requires every referenced field. If a key is missing, Python raises KeyError. That strict behavior is useful when incomplete data should stop the operation immediately.

model = Template("Order $code for $customer")
model.substitute(code="A-10")  # KeyError: customer

safe_substitute, on the other hand, leaves an unresolved placeholder in the output. It is useful for previews, template editors, drafts, and workflows where data arrives in stages.

draft = model.safe_substitute(code="A-10")
print(draft)  # Order A-10 for $customer

The word “safe” does not mean that the result is automatically safe for HTML, SQL, a shell, or another output context. It only avoids an exception for a missing mapping key. Output-specific validation and escaping are still required.

When to prefer Template over f-strings

F-strings are usually the best option when developers control the source code and need expressions, numeric format specifications, or function calls. Template is a better fit when the text lives outside the code in a file, database, CMS, or administration panel.

Its intentionally limited syntax is an advantage. A person editing a message cannot place arbitrary Python expressions inside a placeholder. The application controls the data mapping, while the template controls only presentation. This separation makes content easier to review, translate, and reuse.

For file-based templates, the guides about reading text files and working with JSON provide useful supporting techniques.

Using dictionaries as mappings

substitute accepts a mapping as its first argument. That makes integration with API results, form data, and JSON documents straightforward.

data = {"product": "Python Course", "price": "$99"}
model = Template("$product is available for $price")
print(model.substitute(data))

You can combine a mapping with keyword arguments. Keyword arguments take priority, which allows a base dictionary to provide defaults while a specific call overrides selected values.

defaults = {"company": "Academify", "channel": "website"}
message = Template("$company provides support through the $channel")
print(message.substitute(defaults, channel="WhatsApp"))

Validating placeholders

Recent Python versions provide is_valid() and get_identifiers(). The first checks whether the template syntax is valid. The second returns the placeholder names found in the text.

model = Template("Hello $name, order $code")
if not model.is_valid():
    raise ValueError("Invalid template")
identifiers = model.get_identifiers()
allowed = {"name", "code"}
unknown = set(identifiers) - allowed
if unknown:
    raise ValueError(f"Unknown fields: {unknown}")

This validation is valuable before saving a user-edited template. The application can reject unknown fields, show a clear message, and ensure that every language version uses the expected internal variables. Validation at creation time is usually better than discovering a broken template while sending a customer notification.

Changing the delimiter

You may subclass Template and choose another delimiter when the dollar sign already has a domain-specific meaning.

class AtTemplate(Template):
    delimiter = "@"

model = AtTemplate("Hello @name")
print(model.substitute(name="Carlos"))

The matching pattern can also be customized, but that should be done carefully. A complicated placeholder language increases maintenance cost, documentation needs, and opportunities for parsing errors. Keeping the standard syntax or changing only the delimiter is sufficient for most projects.

Building an email renderer

A practical design stores message bodies in files and keeps validation in Python code. The function below loads a UTF-8 template, checks its syntax, restricts identifiers, and then renders the final result.

from pathlib import Path
from string import Template

def render(path, data):
    content = Path(path).read_text(encoding="utf-8")
    model = Template(content)
    if not model.is_valid():
        raise ValueError("The template syntax is invalid")
    unknown = set(model.get_identifiers()) - set(data)
    if unknown:
        raise ValueError(f"Unsupported fields: {unknown}")
    return model.substitute(data)

This design lets content teams edit wording without modifying the rendering function. The code remains responsible for encoding, validation, approved fields, logging, and error handling. See pathlib in Python for more file-handling patterns.

Security considerations

Template does not evaluate Python expressions, which makes it safer than executing user-provided format expressions. However, it is not a universal sanitization layer. Values inserted into HTML should be escaped. SQL statements should always use the database driver’s parameter mechanism. Shell commands should use argument lists rather than a rendered command string.

Pass the smallest possible mapping. If an object contains private data, do not expose every attribute to a user-controlled template. Build a dedicated dictionary containing only approved values. This prevents a template from requesting information it was never meant to display.

Also decide whether unresolved placeholders should be allowed. A production invoice or transactional email should usually use strict substitute. A visual preview may use safe_substitute so editors can see which fields remain incomplete.

Testing templates

Tests should cover valid rendering, missing keys, repeated placeholders, escaped dollar signs with $$, identifiers next to other characters, and invalid placeholder syntax.

def test_payment_message():
    model = Template("$name paid $$ $amount")
    result = model.substitute(name="Lu", amount="20")
    assert result == "Lu paid $ 20"

Multilingual projects should test every translation with the same approved identifier set. The visible wording changes, but internal field names can remain stable. This keeps the application mapping independent from the selected language.

Formatting values before rendering

Template does not provide the rich numeric formatting syntax found in f-strings. Format values before placing them in the dictionary.

price = 129.9
data = {"price": f"${price:,.2f}", "quantity": str(2)}
message = Template("Quantity: $quantity — Total: $price")
print(message.substitute(data))

This is often a clean design because business formatting stays in tested code while the content model remains simple. The same principle applies to dates, percentages, localized money, and optional values.

Common mistakes

A common mistake is using safe_substitute everywhere and allowing incomplete placeholders to reach customers. Another is passing a large dictionary with unnecessary secrets or internal fields. Teams may also assume that Template escapes HTML or protects SQL, which it does not.

Avoid turning the template syntax into a programming language. If you need loops, conditionals, inheritance, filters, and automatic HTML escaping, a dedicated engine such as Jinja may be more appropriate. Template is strongest when the task is straightforward variable replacement.

Best practices

Use descriptive placeholder names such as $customer_name. Document available fields near the editor. Validate templates when they are created or updated. Keep destination-specific escaping outside the template. Prefer strict substitution for finalized output and safe substitution for previews only.

Version templates with the application or maintain a revision history in the database. A small wording change can accidentally remove a required placeholder, so automated validation should run whenever content is deployed.

Conclusion

string.Template provides a small, readable, and intentionally limited solution for configurable text. It does not replace a full template engine, but it is an excellent fit for notifications, email messages, reports, and simple files. By validating identifiers, exposing only approved values, formatting data before rendering, and applying output-specific escaping, you can create editable templates without mixing presentation with executable logic.

Official references: Python template strings and html.escape.

Share:

Facebook
WhatsApp
Twitter
LinkedIn

Article content

    Related articles

    Synchronized team representing Python asyncio.Barrier
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    asyncio.Barrier: Synchronize Tasks in Phases

    Learn Python asyncio.Barrier to synchronize tasks in phases, coordinate pipelines, avoid races, and handle cancellation safely.

    Ler mais

    Tempo de leitura: 5 minutos
    08/09/2026
    Software developer building models with Python dataclasses.KW_ONLY
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    dataclasses.KW_ONLY: Require Keyword-Only Arguments

    Learn Python dataclasses.KW_ONLY to require named arguments, prevent ambiguous calls, and evolve public APIs more safely.

    Ler mais

    Tempo de leitura: 5 minutos
    08/09/2026
    Software developer using Python operator.methodcaller
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    operator.methodcaller: Call Methods in Pipelines

    Learn Python operator.methodcaller for map, sorted, callbacks, arguments, reusable transformations, and clearer declarative pipelines.

    Ler mais

    Tempo de leitura: 5 minutos
    07/09/2026
    Open folder representing files and directories in Python
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    pathlib.Path.walk: Traverse and Filter Directories

    Learn how to traverse directories with pathlib.Path.walk in Python, filter files, skip folders, handle errors, and avoid common pitfalls.

    Ler mais

    Tempo de leitura: 5 minutos
    07/09/2026
    Python code validated with enum.verify
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    enum.verify: Validate Enum Rules in Python

    Learn Python enum.verify to validate unique values, continuous sequences, and named flags with explicit enum rules.

    Ler mais

    Tempo de leitura: 6 minutos
    06/09/2026
    Software dependency graph and task workflow with Python TopologicalSorter
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    TopologicalSorter: Order Dependencies Safely

    Learn Python TopologicalSorter to order dependencies, detect cycles, and run sequential or parallel pipelines safely.

    Ler mais

    Tempo de leitura: 6 minutos
    06/09/2026