Prompt Templates with Variables

La Lakshmi Priya Updated 05 Nov 2026
12 min read ·Lesson 38 of 40

What Is a Prompt Template With Variables? A prompt template with variables is a prompt stored once in your code, with named placeholders that your program replaces with real values before sending the prompt to the model.

Summarize the text in for {audience} in {bullet_count} bullet points.

{text} At runtime, your code supplies values such as audience="a busy manager", bullet_count=3, and the actual text. The result is a complete prompt.

Lesson 36 designed task-oriented templates. This lesson covers the programming side: the different ways to fill templates in Python, their pitfalls, how to validate variables, how to insert untrusted values safely, and how to organize templates in a project.

Why Use Variables Instead of Writing Prompts Directly? Without Templates With Templates Prompt text is assembled with string concatenation scattered through the code The prompt is defined once in a readable form Each feature may phrase the same task differently Every request uses the same tested wording Changing a rule means finding every copy Change it in one place Easy to forget delimiters or safeguards Safeguards are part of the template Hard to test Templates can be rendered and tested on their own Method 1: f-Strings audience = "a busy manager" text = "The quarterly report shows enrollments grew 26%..."

prompt = f"""Summarize the text in for {audience}.

{text} """ How It Works An f-string is evaluated immediately, using whatever variables exist at that moment in the code.

Limitations Not reusable as a template. The template text and the values are mixed in the same line of code. You cannot store an f-string in a file and fill it later. Hard to test separately. The template only exists where the variables exist. When to Use Small scripts and quick experiments. For anything reused, use one of the following methods.

Method 2: str.format() SUMMARY_TEMPLATE = """Summarize the text in for {audience} in {bullet_count} bullet points.

{text} """

prompt = SUMMARY_TEMPLATE.format( audience="a busy manager", bullet_count=3, text="The quarterly report shows enrollments grew 26%...", ) How It Works The template is a normal string stored in a constant or a file. .format() replaces each {name} with the matching keyword argument.

Pitfall 1: Literal Braces in the Template Prompts often contain JSON examples, which use curly braces:

TEMPLATE = 'Return JSON like {"label": "Positive"}. Review: {review}' TEMPLATE.format(review="Great course") This raises KeyError: '"label"', because .format() treats {"label": "Positive"} as a placeholder. Literal braces must be doubled:

TEMPLATE = 'Return JSON like {{"label": "Positive"}}. Review: {review}' This is easy to forget and makes templates with JSON harder to read.

Pitfall 2: Never Use User Input as the Template

DANGEROUS: never do this

user_template = request.POST["template"] prompt = user_template.format(user=current_user) Format strings can access attributes of the objects passed in, for example {user.class} or deeper attribute chains. If users can control the template string, they may be able to read data from your application objects. Only your own code should define templates; user input should only ever be passed as values.

What Is Safe Values passed into .format() are inserted as plain text and are not re-parsed. A user review containing {audience} is inserted literally, which is the correct behavior.

Method 3: string.Template Python's standard library includes string.Template, which uses $name placeholders instead of braces.

from string import Template

CLASSIFY_TEMPLATE = Template("""Classify the review in as Positive, Negative, or Neutral. Return JSON like {"label": "Positive"}.

$review """)

prompt = CLASSIFY_TEMPLATE.substitute(review="The projects were really useful.") print(prompt) Output:

Classify the review in as Positive, Negative, or Neutral. Return JSON like {"label": "Positive"}.

The projects were really useful. Why It Suits Prompts Curly braces need no escaping, so JSON examples stay readable. substitute() raises KeyError if a variable is missing, so incomplete prompts are caught before they are sent. No attribute access. $name placeholders cannot reach into object attributes, which removes the risk described for .format(). Useful Details safe_substitute() leaves missing placeholders unchanged instead of raising an error. This is rarely what you want for prompts, because an unfilled $audience would be sent to the model. A literal dollar sign is written as $$. Template.get_identifiers() returns the list of placeholder names. It is available from Python 3.11. Method 4: Jinja2 for Complex Templates When templates need loops (for example, a variable number of examples) or conditions (for example, an optional context section), the Jinja2 library is a common choice.

Install:

pip install jinja2 from jinja2 import Environment, StrictUndefined

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

FEW_SHOT_TEMPLATE = env.from_string("""Classify the message as one of: {{ labels | join(", ") }}. {% if context %} Context: {{ context }} {% endif %} Examples: {% for example in examples %} Message: {{ example.message }} Category: {{ example.label }} {% endfor %} Message: {{ message }} Category:""")

prompt = FEW_SHOT_TEMPLATE.render( labels=["Doubt", "Request", "Complaint"], context="Messages come from students on a coding course platform.", examples=[ {"message": "Why does my loop never end?", "label": "Doubt"}, {"message": "Please share today's slides.", "label": "Request"}, ], message="Can I get the recording?", ) print(prompt) Code Explanation Environment(undefined=StrictUndefined, autoescape=False)

StrictUndefined makes Jinja2 raise an error when a variable is missing. Without it, a missing variable silently becomes an empty string, and the model would receive a prompt with a gap. autoescape=False turns off HTML escaping. Autoescaping is designed for web pages; in prompts, it would turn < into < and break your XML-style delimiters. {{ labels | join(", ") }} Inserts the label list as a comma-separated string using a Jinja2 filter.

{% if context %} Includes the context section only when a value is provided, so prompts without context do not contain an empty "Context:" line.

{% for example in examples %} Renders any number of examples in an identical format, which guarantees consistent example formatting (Lesson 29).

Whitespace Note Jinja2 block tags such as {% if %} leave blank lines in the output. These are usually harmless in prompts, but if you want precise control, Jinja2 supports whitespace control with {%- and -%}, or the trim_blocks and lstrip_blocks environment options.

Warning: As with .format(), never render a template string supplied by users. Jinja2 templates can execute expressions. User input must only be passed as variables to render().

Choosing a Method Method Braces in Text Loops and Conditions Missing Variable Best For f-string Must escape Python code around it Python error at creation Quick scripts str.format() Must double {{ }} No KeyError Simple templates without JSON string.Template No escaping needed No KeyError with substitute() Most single-prompt templates Jinja2 No escaping needed Yes Error with StrictUndefined Templates with examples, optional sections, lists Validating Variables Before Rendering Filling a template is not only about substitution. The values must also be valid. Common checks include:

Check Why Required variables present Prevents incomplete prompts Values are not empty An empty text produces a meaningless request Allowed values tone must be one of your supported tones Length limits Very long inputs can exceed the context window or budget Type and range bullet_count must be an integer between 1 and 10 A Reusable Template Class The following class combines string.Template with validation and safe wrapping of untrusted values.

from string import Template

class PromptTemplate: def init(self, text: str, required: set[str], max_chars: dict[str, int] | None = None): self.template = Template(text) self.required = required self.max_chars = max_chars or {}

def render(self, **values) -> str: missing = self.required - values.keys() if missing: raise ValueError(f"Missing variables: {sorted(missing)}")

for name, value in values.items(): text = str(value).strip() if name in self.required and not text: raise ValueError(f"Variable '{name}' is empty") limit = self.max_chars.get(name) if limit and len(text) > limit: raise ValueError(f"Variable '{name}' exceeds {limit} characters")

return self.template.substitute(values)

SUMMARY = PromptTemplate( text="""Summarize the text in for $audience. Write exactly $bullet_count bullet points. Include only information stated in the text.

$text """, required={"audience", "bullet_count", "text"}, max_chars={"text": 20_000, "audience": 200}, )

prompt = SUMMARY.render( audience="a manager who missed the meeting", bullet_count=3, text="Decision: the Django batch will start on 15 November...", ) print(prompt) Code Explanation init Stores the template, the set of required variables, and optional character limits per variable.

Missing-variable check self.required - values.keys() uses set difference to find required variables that were not provided. The error lists all of them at once, which is easier to debug than one error at a time.

Empty and length checks Each value is converted to a string and stripped of whitespace. Required values must not be empty, and values with a limit must not exceed it. The limit on text protects against inputs that are too large for the model or your budget.

substitute(values) Performs the actual replacement. Because all required variables were verified, substitute() will only fail if the template contains a placeholder that was not declared as required, which indicates a mistake in the template definition and should fail loudly.

Type hint dict[str, int] | None Uses the | union syntax available from Python 3.10.

Inserting Untrusted Values Safely Variables often contain user-provided text: messages, reviews, documents. Three rules keep templates safe:

Rule 1: Delimit Every Untrusted Variable Place untrusted values inside named delimiters in the template, as $text is inside above. The template author decides the delimiters; users only supply content (Lesson 37).

Rule 2: Remove Delimiter Collisions If a user's text contains your closing tag, remove it before rendering, or use random tag suffixes (Lesson 37):

def clean_for_tag(value: str, tag: str) -> str: return value.replace(f"</{tag}>", "").replace(f"<{tag}>", "") Rule 3: Substitute Only Once Never render a template, then render the result again with more values. After the first rendering, the prompt contains user text, and a second rendering would interpret any $name or {name} inside that user text as a placeholder. Collect all values first and render once.

Organizing Templates in a Project For applications with several prompts, keep templates as files, separate from code.

my_app/ ├── prompts/ │ ├── summarize.txt │ ├── classify_ticket.txt │ └── extract_job.txt ├── prompting/ │ ├── init.py │ └── templates.py └── tests/ └── test_templates.py from pathlib import Path from string import Template

PROMPTS_DIR = Path(file).resolve().parent.parent / "prompts"

def load_template(name: str) -> Template: text = (PROMPTS_DIR / f"{name}.txt").read_text(encoding="utf-8") return Template(text) Why This Structure Helps Prompt changes appear clearly in version control and code review (Lesson 67). Non-developers, such as content or support specialists, can review the wording. Tests can render every template with sample values to confirm they still work. Testing Templates A simple test renders each template with valid values and checks the result:

def test_summary_template_renders(): prompt = SUMMARY.render(audience="students", bullet_count=3, text="Sample text.") assert "\nSample text.\n" in prompt assert "$" not in prompt # no unfilled placeholders

def test_summary_template_rejects_missing_text(): import pytest with pytest.raises(ValueError): SUMMARY.render(audience="students", bullet_count=3) These tests use pytest (pip install pytest). The first confirms correct rendering and that no $ placeholders remain; the second confirms that a missing variable is rejected. The "$" not in prompt check assumes your sample values contain no dollar signs.

Common Mistakes Mistake 1: Using str.format() With JSON in the Template What it is: A template containing JSON examples filled with .format().

Why it happens: .format() is the most familiar method.

What it causes: KeyError exceptions, or templates full of doubled braces that are hard to read.

How to fix it: Use string.Template or Jinja2 for templates that contain braces.

Mistake 2: Silent Missing Variables What it is: Using safe_substitute() or Jinja2's default undefined behavior.

Why it happens: It avoids errors during development.

What it causes: Prompts with gaps or raw placeholders reach the model, which guesses.

How to fix it: Use substitute() or StrictUndefined so missing variables fail loudly.

Mistake 3: Treating User Input as a Template What it is: Calling .format() or Jinja2 from_string() on text supplied by users.

Why it happens: Features that let users customize prompts are built quickly.

What it causes: Possible exposure of application data or execution of template expressions.

How to fix it: Templates come only from your code; user content is passed only as values.

Mistake 4: Rendering Twice What it is: Filling some variables, then filling the result again with more.

Why it happens: Values become available at different stages of the code.

What it causes: User text containing placeholder syntax is interpreted as a placeholder.

How to fix it: Collect all values first, then render once.

Mistake 5: No Validation of Values What it is: Accepting any value for any variable.

Why it happens: Validation seems unnecessary when calling your own code.

What it causes: Empty inputs, oversized documents, or unsupported option values produce poor or failed requests.

How to fix it: Validate presence, emptiness, allowed values, and length before rendering.

Best Practices Store templates separately from the values. Constants or files, not inline f-strings, for anything reused.

Prefer string.Template for prompts with JSON. Use Jinja2 when you need loops or optional sections.

Fail loudly on missing variables. substitute() or StrictUndefined.

Validate values before rendering. Required, non-empty, allowed, and within limits.

Delimit untrusted variables in the template. And remove delimiter collisions from their values.

Never treat user input as a template. Users supply values, never template text.

Render once. Collect all values, then substitute in a single step.

Keep templates in version control and test them. Render each one with sample values in automated tests.

Try It Yourself Exercise 1: Convert to string.Template Convert this .format() template to string.Template and fill it with sample values:

TEMPLATE = 'Extract fields as JSON like {{"name": null, "city": null}}. Message: {message}' Confirm that the JSON example no longer needs doubled braces.

Exercise 2: Extend PromptTemplate Add an allowed parameter to PromptTemplate, a dictionary mapping variable names to sets of allowed values, such as {"tone": {"formal", "friendly"}}. Make render() raise an error for any value outside its allowed set, and write a test for it.

Exercise 3: Build a Few-Shot Template With Jinja2 Using Jinja2 with StrictUndefined, create a template that renders an instruction, an optional context section, a list of examples wrapped in tags, and the real input wrapped in tags. Render it once with context and three examples, and once without context, and check the output for unwanted blank lines.

0 Comments

Reviewed before they appear

No comments yet.

Prompt Engineering
Ask about this post
AI Ask about this post

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

Signed-in readers only.