Prompt Variables and Templating: Dynamic Content Injection with Jinja2 and f-strings
Master prompt templating techniques using Jinja2 and Python f-strings. Learn variable injection patterns, conditional blocks, loop constructs, custom filters, and safety practices for dynamic prompts.
Why Static Prompts Fall Short
Hardcoded prompts work for demos. Production agents need prompts that adapt — inserting the user's name, adjusting tone based on context, including relevant data, and conditionally enabling features. This is prompt templating: defining a prompt structure once and injecting dynamic values at runtime.
The two dominant approaches in Python are f-strings for simple cases and Jinja2 for complex logic. Understanding when to use each prevents both over-engineering and under-engineering your prompt layer.
f-string Templating: Simple and Direct
For prompts with straightforward variable substitution, Python f-strings are the fastest path.
def build_support_prompt(
user_name: str,
account_tier: str,
issue_summary: str
) -> str:
"""Build a support agent prompt with user context."""
return f"""You are a customer support agent for Acme Corp.
The customer's name is {user_name}.
Their account tier is {account_tier}.
Issue summary: {issue_summary}
Respond helpfully and professionally. If the customer
has a Premium or Enterprise tier, prioritize their request
and offer direct escalation options."""
This is readable and type-safe — your IDE catches missing variables. However, f-strings hit limits quickly. You cannot loop over lists of items, conditionally include sections, or reuse template fragments.
Jinja2 Templating: Full Power
Jinja2 gives you conditionals, loops, filters, template inheritance, and macros. It is the standard for complex prompt templating.
from jinja2 import Environment, FileSystemLoader, select_autoescape
class PromptTemplateEngine:
"""Render prompts using Jinja2 templates."""
def __init__(self, templates_dir: str = "prompt_templates"):
self.env = Environment(
loader=FileSystemLoader(templates_dir),
autoescape=select_autoescape(default=False),
trim_blocks=True,
lstrip_blocks=True,
)
def render(
self, template_name: str, **variables
) -> str:
"""Render a named template with variables."""
template = self.env.get_template(template_name)
return template.render(**variables)
Store templates as separate files.
See AI Voice Agents Handle Real Calls
Book a free demo or calculate how much you can save with AI voice automation.
# prompt_templates/support_agent.md.j2
# ---
# Template: support_agent
# Variables: user_name, account_tier, conversation_history,
# available_tools, escalation_allowed
# ---
You are a customer support agent for Acme Corp.
Customer: {{ user_name }} ({{ account_tier }} tier)
{% if conversation_history %}
## Previous Conversation
{% for msg in conversation_history %}
{{ msg.role | upper }}: {{ msg.content }}
{% endfor %}
{% endif %}
## Available Actions
{% for tool in available_tools %}
- {{ tool.name }}: {{ tool.description }}
{% endfor %}
{% if account_tier in ["premium", "enterprise"] %}
This is a high-priority customer. You may offer:
- Direct phone callback within 1 hour
- Escalation to a senior specialist
{% endif %}
{% if not escalation_allowed %}
Note: Do NOT offer escalation options in this session.
{% endif %}
# Usage
engine = PromptTemplateEngine()
prompt = engine.render(
"support_agent.md.j2",
user_name="Alice Chen",
account_tier="premium",
conversation_history=[
{"role": "user", "content": "My invoice is wrong"},
{"role": "assistant", "content": "Let me look into that."},
],
available_tools=[
{"name": "lookup_invoice", "description": "Fetch invoice details"},
{"name": "create_ticket", "description": "Open a support ticket"},
],
escalation_allowed=True,
)
Custom Filters for Prompt-Specific Needs
Jinja2 filters transform values inline. Add custom filters for common prompt operations.
def setup_prompt_filters(env: Environment):
"""Add prompt-specific Jinja2 filters."""
def truncate_tokens(text: str, max_tokens: int = 500) -> str:
"""Rough truncation by word count as a token proxy."""
words = text.split()
if len(words) <= max_tokens:
return text
return " ".join(words[:max_tokens]) + "..."
def format_list(items: list, style: str = "bullet") -> str:
"""Format a list for prompt readability."""
if style == "numbered":
return "\n".join(
f"{i+1}. {item}" for i, item in enumerate(items)
)
return "\n".join(f"- {item}" for item in items)
def mask_pii(text: str) -> str:
"""Mask email addresses and phone numbers."""
import re
text = re.sub(
r'[\w.+-]+@[\w-]+\.[\w.]+', '[EMAIL]', text
)
text = re.sub(
r'\b\d{3}[-.]?\d{3}[-.]?\d{4}\b', '[PHONE]', text
)
return text
env.filters["truncate_tokens"] = truncate_tokens
env.filters["format_list"] = format_list
env.filters["mask_pii"] = mask_pii
Use them in templates: {{ user_message | mask_pii | truncate_tokens(200) }}.
Safety Practices
Dynamic prompts introduce injection risks. User-provided values could contain instructions that hijack the agent's behavior.
class SafePromptRenderer:
"""Render prompts with input sanitization."""
def __init__(self, engine: PromptTemplateEngine):
self.engine = engine
def sanitize_input(self, value: str) -> str:
"""Remove patterns that could be prompt injections."""
dangerous_patterns = [
"ignore previous instructions",
"ignore all instructions",
"disregard the above",
"new instructions:",
"system:",
"ADMIN OVERRIDE",
]
sanitized = value
for pattern in dangerous_patterns:
sanitized = sanitized.replace(
pattern, "[FILTERED]"
)
return sanitized
def render_safe(
self, template_name: str, **variables
) -> str:
"""Render with all string variables sanitized."""
safe_vars = {}
for key, value in variables.items():
if isinstance(value, str):
safe_vars[key] = self.sanitize_input(value)
else:
safe_vars[key] = value
return self.engine.render(template_name, **safe_vars)
Always sanitize user-provided inputs before injecting them into prompts. Treat prompt templates like SQL queries — never insert raw user input without validation.
FAQ
When should I use f-strings versus Jinja2?
Use f-strings when your prompt has fewer than five variables and no conditional logic. Switch to Jinja2 when you need conditionals, loops, template inheritance, or when non-engineers need to edit the templates. The readability of Jinja2 templates makes them better for team collaboration.
How do I handle missing template variables?
Configure Jinja2 with undefined=StrictUndefined to raise errors on missing variables rather than silently inserting empty strings. This catches bugs during development. In production, you can use default filters: {{ user_name | default("Customer") }}.
Can prompt injection be fully prevented with sanitization?
No. Blocklist-based sanitization catches known patterns but misses creative bypasses. Layer multiple defenses: sanitize inputs, use structured system-vs-user message separation, validate outputs, and monitor for anomalous agent behavior. Sanitization is one layer in a defense-in-depth strategy.
#PromptTemplating #Jinja2 #Python #DynamicPrompts #PromptEngineering #AgenticAI #LearnAI #AIEngineering
CallSphere Team
Expert insights on AI voice agents and customer communication automation.
Try CallSphere AI Voice Agents
See how AI voice agents work for your industry. Live demo available -- no signup required.