Use Prompt Templates

Su Suriya Ravichandran Updated 10 Nov 2026
9 min read ·Lesson 34 of 200

What a Prompt Template Is

A prompt template is a prompt with placeholders for the parts that change between requests. Your code fills in the placeholders with real values, such as a document, a user's question, or a product name, to produce the final prompt.

TEMPLATE = "Summarize the following article in {sentences} sentences:\n\n<article>\n{article}\n</article>"

prompt = TEMPLATE.format(sentences=3, article=article_text)

The template is written once. The data changes with every request.

Templates appeared briefly in earlier lessons (17, 22, 32). This lesson covers them properly: why they matter, the main ways to build them in Python, how to avoid common pitfalls such as brace conflicts and missing variables, and how to manage templates in a real project.


Why Use Templates

BenefitExplanation
ConsistencyEvery request uses the same tested wording, structure, and instructions
SeparationPrompt wording (owned by whoever designs prompts) is separate from code logic and from data
MaintainabilityImproving the prompt means editing one template, not hunting through code
TestabilityYou can render a template with sample values and check the result before sending anything
VersioningTemplates can be stored, reviewed, and versioned like code (Lesson 32)
SafetyA fixed structure keeps user data inside designated tags, separate from instructions

Approach 1: f-Strings

def review_prompt(code: str, language: str) -> str:
    return f"""Review this {language} code for bugs and readability.

<code>
{code}
</code>

List each problem with a one-sentence fix."""
  • Advantages: simple, readable, and checked by your editor. Variables must exist when the function runs.
  • Limitations: the template lives inside code. It cannot be loaded from a file or edited without changing Python code.

f-strings are a good choice for small templates that are used in one place.


Approach 2: str.format() with Stored Templates

A template stored as a normal string, for example in a constant or a file, can be filled later with .format():

SUMMARY_TEMPLATE = """Summarize the following article in {sentences} sentences for {audience}.

<article>
{article}
</article>"""

prompt = SUMMARY_TEMPLATE.format(sentences=3, audience="busy managers", article=article_text)

The Brace Problem

.format() treats every {...} in the template as a placeholder. Templates that contain literal braces, such as JSON examples or code, break:

TEMPLATE = 'Return JSON like {"name": "...", "age": 0} for this person: {person}'
TEMPLATE.format(person="Asha, 29")   # KeyError: '"name"'

Literal braces must be doubled:

TEMPLATE = 'Return JSON like {{"name": "...", "age": 0}} for this person: {person}'

This is easy to forget and makes templates with code or JSON hard to read.

Values Are Not Re-Parsed

Braces inside the values you pass are safe. .format() does not interpret the substituted text:

"<code>\n{code}\n</code>".format(code="data = {'a': 1}")   # Works fine

The problem only arises when braces appear in the template text itself, or when you concatenate user input into a template before calling .format(), which turns user input into template text.


Approach 3: string.Template

Python's standard library includes string.Template, which uses $name placeholders instead of braces. Braces in the template are just ordinary characters, which makes it well suited to prompts that contain JSON or code:

from string import Template

EXTRACT_TEMPLATE = Template("""Extract the contact details from the text below.
Return JSON in exactly this shape: {"name": "...", "email": "...", "phone": "..."}

<text>
$text
</text>""")

prompt = EXTRACT_TEMPLATE.substitute(text="Call Ravi at 98765 43210 or ravi@example.com")
MethodBehavior when a variable is missing
substitute(...)Raises KeyError, so a mistake is caught immediately
safe_substitute(...)Leaves the placeholder in the text, which is usually a hidden bug

Prefer substitute() for prompts, so a missing value fails loudly instead of silently sending $text to Claude.

Use ${name} when a placeholder is directly followed by letters, as in ${count}th.


Approach 4: Jinja2 for Complex Templates

When templates need loops or conditions, for example to render a variable number of documents or optional sections, the popular Jinja2 library is a good fit:

python -m pip install jinja2
from jinja2 import Environment, StrictUndefined

env = Environment(undefined=StrictUndefined, autoescape=False)

QA_TEMPLATE = env.from_string("""<documents>
{% for doc in documents %}
  <document index="{{ loop.index }}">
    <source>{{ doc.source }}</source>
    <document_content>{{ doc.content }}</document_content>
  </document>
{% endfor %}
</documents>

{% if audience %}Write for {{ audience }}.{% endif %}
Answer using only the documents above.
<question>{{ question }}</question>""")

prompt = QA_TEMPLATE.render(
    documents=[{"source": "faq.md", "content": "Sessions run Mondays at 7 PM IST."}],
    question="When are the sessions?",
    audience="new students",
)

Important settings:

  • undefined=StrictUndefined makes rendering fail with an error if a variable is missing. Jinja2's default is to silently render an empty string.
  • autoescape=False is correct for prompts. Autoescaping is for HTML output and would turn characters such as < into <, damaging your XML-style tags.
  • {% for %} and {% if %} handle variable numbers of documents and optional sections cleanly.

Comparison

ApproachPlaceholderBraces in templateLoops/conditionsMissing variableBest for
f-string{name}Must double themPython code around itNameError at runtimeSmall inline prompts
str.format(){name}Must double themNoKeyErrorSimple stored templates
string.Template$nameFine as-isNoKeyError with substitute()Stored templates with JSON or code
Jinja2{{ name }}Fine as-isYesError with StrictUndefinedComplex, structured templates

Keep Data Inside Tags

Wherever possible, place each variable inside a clearly named tag rather than in the middle of an instruction:

Weak:    Translate {text} into French.
Better:  Translate the text inside <text> tags into French.
         <text>
         {text}
         </text>

Why: if the inserted text is long, contains instructions, or contains unusual characters, the tags keep it clearly separate from your own instructions. Lesson 40 covers how this helps against prompt injection.

Warning: Never render templates supplied by users with a template engine. Template syntax can execute expressions, so user-provided templates are a security risk known as template injection. Users supply values; your application supplies templates.


Managing Templates in a Project

As an application grows, keep templates out of the code that uses them:

myapp/
├── prompts/
│   ├── summarize_v2.txt
│   ├── classify_ticket_v1.txt
│   └── extract_contact_v1.txt
└── prompting.py        # Loads, validates, and renders templates

Good practices for template files:

  • Name templates by purpose and version, so changes are deliberate and traceable.
  • Document required variables at the top of the file, or validate them automatically.
  • Test templates by rendering them with sample values before deployment.
  • Log the template name and version with each request, alongside the message ID (Lesson 29).

Practical Example: A Validated Template Class

This class loads templates written in string.Template syntax, detects their required variables, validates inputs before rendering, and records which template version was used.

"""prompting.py: load, validate, and render versioned prompt templates."""

import re
from dataclasses import dataclass
from string import Template

from anthropic import Anthropic
from anthropic.types import Message

PLACEHOLDER = re.compile(r"\$(?:(\w+)|\{(\w+)\})")


@dataclass(frozen=True)
class PromptTemplate:
    name: str
    version: int
    text: str

    @property
    def variables(self) -> set[str]:
        return {a or b for a, b in PLACEHOLDER.findall(self.text)}

    def render(self, **values: str) -> str:
        missing = self.variables - values.keys()
        extra = values.keys() - self.variables
        if missing:
            raise ValueError(f"{self.name} v{self.version}: missing variables {sorted(missing)}")
        if extra:
            raise ValueError(f"{self.name} v{self.version}: unexpected variables {sorted(extra)}")
        return Template(self.text).substitute(**{k: str(v) for k, v in values.items()})


CLASSIFY_TICKET = PromptTemplate(
    name="classify_ticket",
    version=1,
    text="""Classify the support ticket below for the $product team.
Reply with JSON exactly like {"category": "...", "urgent": true}, where category is one of:
billing, bug, account, other.

<ticket>
$ticket
</ticket>""",
)


def get_text(message: Message) -> str:
    return "".join(block.text for block in message.content if block.type == "text").strip()


if __name__ == "__main__":
    print("Variables:", sorted(CLASSIFY_TICKET.variables))

    prompt = CLASSIFY_TICKET.render(
        product="GoCourse",
        ticket="I paid yesterday but my course still shows as locked!",
    )

    reply = Anthropic().messages.create(
        model="claude-opus-5-5",
        max_tokens=1024,
        messages=[{"role": "user", "content": prompt}],
    )
    print(f"[{CLASSIFY_TICKET.name} v{CLASSIFY_TICKET.version}] {get_text(reply)}")

    try:
        CLASSIFY_TICKET.render(ticket="Missing product variable")
    except ValueError as error:
        print("Validation:", error)

Example output:

Variables: ['product', 'ticket']
[classify_ticket v1] {"category": "billing", "urgent": true}
Validation: classify_ticket v1: missing variables ['product']

How It Works

  1. PLACEHOLDER is a regular expression that matches both $name and ${name}. findall() returns pairs of capture groups, and a or b picks whichever group matched.
  2. variables lists the placeholders the template needs. It is computed from the text, so it is always in sync with the template.
  3. render() compares the provided values with the required variables. Missing and unexpected variables are both errors. An unexpected variable usually means a typo, such as tickets instead of ticket.
  4. Template(...).substitute() fills in the values. The JSON example in the template contains braces, which string.Template leaves untouched, so no doubling is needed.
  5. frozen=True prevents a template from being modified at runtime, and name and version make every request traceable to the exact template that produced it.

Note: The JSON format here is requested through instructions only. Lesson 36 explains why that can occasionally fail, and how structured outputs guarantee valid JSON.


Common Mistakes

Unescaped Braces in .format() Templates

What happens: KeyError or ValueError when the template contains JSON or code.

Fix: double the braces, or use string.Template or Jinja2.

Concatenating User Input into the Template Before Formatting

template = "Summarize: " + user_text + " in {n} sentences"
template.format(n=3)   # Breaks if user_text contains braces

Fix: pass user input as a value: "Summarize: {text} in {n} sentences".format(text=user_text, n=3).

Silently Missing Variables

What happens: safe_substitute() or Jinja2's default undefined handling sends prompts with empty or literal placeholders, and Claude receives an incomplete prompt.

Fix: use substitute(), StrictUndefined, or explicit validation.

Autoescaping Prompt Templates

What happens: Jinja2 with HTML autoescaping turns <document> into <document>.

Fix: set autoescape=False for prompt templates.

Rendering User-Supplied Templates

What happens: template injection, a security vulnerability.

Fix: only render templates written by your application. Treat user input strictly as values.


Best Practices

  • Use templates for every prompt that runs more than once.
  • Choose the simplest tool that fits: f-strings for inline prompts, string.Template for stored prompts with JSON or code, and Jinja2 for loops and conditions.
  • Fail loudly on missing variables.
  • Place each variable inside a named tag.
  • Store templates in versioned files, and log their versions with requests.
  • Render and inspect templates in tests before deploying changes.
  • Never treat user input as template text.

Exercise

  1. Write a str.format() template that asks for JSON output and includes an example JSON object. Make it work by escaping the braces correctly.
  2. Convert the same template to string.Template. Which version is easier to read?
  3. Create a Jinja2 template that renders a list of 1–5 code files, each in a <file path="..."> tag, followed by a review instruction. Render it with two files.
  4. Add a preview(**values) method to PromptTemplate that returns the rendered prompt with each value shortened to at most 40 characters, which is useful for logging.
Show answers for steps 1 and 4

Step 1:

TEMPLATE = 'Describe this person as JSON like {{"name": "...", "age": 0}}:\n<person>{person}</person>'
print(TEMPLATE.format(person="Asha, 29, data analyst"))

{{ and }} render as literal { and }.

Step 4:

def preview(self, **values: str) -> str:
    short = {key: (str(value)[:37] + "...") if len(str(value)) > 40 else str(value)
             for key, value in values.items()}
    return self.render(**short)

Shortening values keeps logs readable and avoids storing long, possibly sensitive user text, while still showing how the template was filled.

0 Comments

Reviewed before they appear

No comments yet.

Anthropic SDK Using Python
Anthropic SDK Intro Claude API Intro SDK Requirements Install Anthropic SDK Upgrade the SDK Create an API Key Set API Keys Use Environment Variables Import Anthropic Create a Client Send Your First Message Choose a Claude Model Set Max Tokens Read a Response Print Response Text Understand Message Blocks Use System Prompts Send User Messages Set Temperature Handle Stop Reasons Messages API Intro Create Messages Use Message Roles Send Conversation History Continue a Conversation Build a Chat Loop Reset Chat History Use Multiple Turns Inspect Message IDs Read Usage Data Write Clear Prompts Set a System Prompt Add Context to Prompts Use Prompt Templates Add Examples to Prompts Request JSON Output Use XML Tags Control Response Length Improve Prompt Quality Avoid Prompt Injection Read Text Blocks Read Content Blocks Check Stop Reasons Count Input Tokens Count Output Tokens Count Tokens Before Sending Limit Output Tokens Use Stop Sequences Read Request IDs Parse API Responses Streaming Intro Stream Text Output Use Stream Events Use Stream Helpers Get Final Messages Stream Async Responses Handle Stream Errors Build a Live Chat Stream Long Responses Close Streams Safely Build a Chatbot Keep Chat History Manage Chat Sessions Add Chat Memory Build a CLI Chatbot Build a Web Chatbot Use Flask with Claude Use FastAPI with Claude Return JSON from an API Add Chat Error Messages Async SDK Intro Create an Async Client Send Async Messages Use asyncio Run Parallel Requests Stream Async Text Handle Async Errors Set Async Timeouts Close Async Clients Choose Sync or Async Tool Use Intro Define a Tool Create Tool Schemas Send Tools to Claude Read Tool Calls Run Python Tools Return Tool Results Continue After Tool Use Control Tool Choice Handle Tool Errors Use the beta_tool Decorator Create a Tool Runner Add Tool Docstrings Validate Tool Inputs Use Multiple Tools Limit Tool Calls Log Tool Activity Build a Weather Tool Build a Search Tool Build a Calculator Tool Structured Output Intro Request JSON Data Validate JSON Output Use Pydantic Models Handle Invalid JSON Extract Fields from Text Generate Typed Data Use Image URLs Send Base64 Images Read PDFs with Claude Use Output Schemas Build Data Extraction Test Structured Responses Send Image Inputs Upload Files Use File IDs Process Text Files Summarize Documents Extract Table Data Handle File Errors Read Cache Usage Reduce Repeated Input Costs Debug Cache Misses Prompt Caching Intro Enable Prompt Caching Cache System Prompts Cache Long Context Test Cache Performance Use Cache Breakpoints Cache Best Practices Message Batches Intro Create a Message Batch Set Batch IDs Check Batch Status List Message Batches Get Batch Results Process Batch Outputs Handle Failed Batch Items Use Batches for Bulk Tasks Compare Batches and Requests Handle API Errors Handle Rate Limits Retry Failed Requests Set Request Timeouts Handle Network Errors Use Exponential Backoff Build Reliable API Calls Use Extended Thinking Log API Errors Track Request IDs Paginate API Results Set Thinking Budgets Read Thinking Blocks Use Tool Search Use Web Search Tools Use Computer Tools Use Citations Use Token Counting Read Raw Responses Set Custom Headers Use Claude on AWS Bedrock Use Claude on Vertex AI Use Claude on Microsoft Foundry Configure Cloud Credentials Choose a Cloud Provider Compare API Integrations Configure Provider Clients Handle Cloud Errors Use Cloud SDK Extras Deploy Cloud Apps Protect API Keys Use a .env File Ignore Secrets in Git Validate User Input Reduce Prompt Injection Limit Tool Permissions Avoid Unsafe Tool Calls Sanitize Tool Results Set Usage Limits Review Security Logs Test SDK Functions Mock API Responses Test Error Handling Test Streaming Code Measure Token Usage Track API Costs Configure Logging Deploy with Docker Deploy a FastAPI App Monitor Production Apps Build a Claude Chatbot Build a Text Summarizer Build a Code Explainer Build a JSON Extractor Build a PDF Assistant Build a Streaming Chat App Build a Tool-Calling Agent Build a Batch Processor Build a Research Assistant Build a Claude API Dashboard
Ask about this post
AI Ask about this post

Ask questions about Use Prompt Templates and get answers drawn from it.

Signed-in readers only.