Introduction: Prompts are the interface between your application and LLMs. As applications grow, managing prompts becomes challenging—they’re scattered across code, hard to version, and difficult to test. A prompt template system brings order to this chaos. It separates prompt logic from application code, enables versioning and A/B testing, and makes prompts reusable across different contexts. This guide covers practical template management: designing flexible templates, implementing version control, building template registries, and creating systems that let you iterate on prompts without deploying code.

Template Design
from dataclasses import dataclass, field
from typing import Any, Optional, Callable
import re
from string import Template as StringTemplate
@dataclass
class PromptTemplate:
"""A prompt template with variables."""
name: str
template: str
description: str = ""
# Variable definitions
variables: dict[str, dict] = field(default_factory=dict)
# Metadata
version: str = "1.0.0"
tags: list[str] = field(default_factory=list)
def render(self, **kwargs) -> str:
"""Render template with variables."""
# Validate required variables
for var_name, var_def in self.variables.items():
if var_def.get("required", True) and var_name not in kwargs:
if "default" in var_def:
kwargs[var_name] = var_def["default"]
else:
raise ValueError(f"Missing required variable: {var_name}")
# Apply transformations
for var_name, value in kwargs.items():
if var_name in self.variables:
transform = self.variables[var_name].get("transform")
if transform:
kwargs[var_name] = transform(value)
# Render using Python format strings
return self.template.format(**kwargs)
def get_variables(self) -> list[str]:
"""Extract variable names from template."""
pattern = r'\{(\w+)\}'
return list(set(re.findall(pattern, self.template)))
class JinjaPromptTemplate:
"""Prompt template using Jinja2 for complex logic."""
def __init__(
self,
name: str,
template: str,
description: str = ""
):
from jinja2 import Environment, BaseLoader
self.name = name
self.template_str = template
self.description = description
self.env = Environment(loader=BaseLoader())
self.template = self.env.from_string(template)
def render(self, **kwargs) -> str:
"""Render template with Jinja2."""
return self.template.render(**kwargs)
# Example templates
SUMMARIZATION_TEMPLATE = PromptTemplate(
name="summarization",
template="""Summarize the following text in {style} style.
Text:
{text}
Summary length: {length} words
Focus on: {focus}
Summary:""",
description="Summarize text with configurable style and length",
variables={
"text": {"required": True, "description": "Text to summarize"},
"style": {"required": False, "default": "concise", "description": "Writing style"},
"length": {"required": False, "default": 100, "description": "Target word count"},
"focus": {"required": False, "default": "key points", "description": "What to focus on"}
},
version="1.0.0",
tags=["summarization", "text-processing"]
)
QA_TEMPLATE = PromptTemplate(
name="question_answering",
template="""Answer the question based on the provided context.
Context:
{context}
Question: {question}
Instructions:
- Only use information from the context
- If the answer is not in the context, say "I don't have enough information"
- Be {tone} in your response
Answer:""",
description="Answer questions based on provided context",
variables={
"context": {"required": True},
"question": {"required": True},
"tone": {"required": False, "default": "helpful and professional"}
},
version="1.0.0",
tags=["qa", "rag"]
)
Template Registry
from dataclasses import dataclass, field
from typing import Any, Optional
from datetime import datetime
import json
@dataclass
class TemplateVersion:
"""A specific version of a template."""
version: str
template: PromptTemplate
created_at: datetime
created_by: str = None
changelog: str = None
is_active: bool = True
class TemplateRegistry:
"""Registry for managing prompt templates."""
def __init__(self):
self._templates: dict[str, dict[str, TemplateVersion]] = {}
self._active_versions: dict[str, str] = {}
def register(
self,
template: PromptTemplate,
created_by: str = None,
changelog: str = None
) -> TemplateVersion:
"""Register a new template or version."""
name = template.name
version = template.version
if name not in self._templates:
self._templates[name] = {}
# Create version entry
template_version = TemplateVersion(
version=version,
template=template,
created_at=datetime.utcnow(),
created_by=created_by,
changelog=changelog
)
self._templates[name][version] = template_version
# Set as active if first version
if name not in self._active_versions:
self._active_versions[name] = version
return template_version
def get(
self,
name: str,
version: str = None
) -> Optional[PromptTemplate]:
"""Get a template by name and optional version."""
if name not in self._templates:
return None
if version is None:
version = self._active_versions.get(name)
if version not in self._templates[name]:
return None
return self._templates[name][version].template
def set_active(self, name: str, version: str):
"""Set the active version for a template."""
if name not in self._templates:
raise ValueError(f"Template not found: {name}")
if version not in self._templates[name]:
raise ValueError(f"Version not found: {version}")
self._active_versions[name] = version
def list_templates(self) -> list[dict]:
"""List all templates with their active versions."""
return [
{
"name": name,
"active_version": self._active_versions.get(name),
"versions": list(versions.keys()),
"description": versions[self._active_versions.get(name)].template.description
if self._active_versions.get(name) else None
}
for name, versions in self._templates.items()
]
def get_versions(self, name: str) -> list[TemplateVersion]:
"""Get all versions of a template."""
if name not in self._templates:
return []
return sorted(
self._templates[name].values(),
key=lambda v: v.created_at,
reverse=True
)
def render(
self,
name: str,
version: str = None,
**kwargs
) -> str:
"""Render a template by name."""
template = self.get(name, version)
if template is None:
raise ValueError(f"Template not found: {name}")
return template.render(**kwargs)
class FileTemplateRegistry(TemplateRegistry):
"""Registry that persists templates to files."""
def __init__(self, base_path: str):
super().__init__()
self.base_path = base_path
self._load_templates()
def _load_templates(self):
"""Load templates from disk."""
import os
if not os.path.exists(self.base_path):
os.makedirs(self.base_path)
return
for filename in os.listdir(self.base_path):
if filename.endswith('.json'):
filepath = os.path.join(self.base_path, filename)
with open(filepath, 'r') as f:
data = json.load(f)
template = PromptTemplate(**data)
self.register(template)
def save(self, template: PromptTemplate):
"""Save template to disk."""
import os
filename = f"{template.name}_{template.version}.json"
filepath = os.path.join(self.base_path, filename)
with open(filepath, 'w') as f:
json.dump({
"name": template.name,
"template": template.template,
"description": template.description,
"variables": template.variables,
"version": template.version,
"tags": template.tags
}, f, indent=2)
Template Composition
from dataclasses import dataclass
from typing import Any, Optional, Callable
@dataclass
class TemplateComponent:
"""A reusable template component."""
name: str
content: str
description: str = ""
class ComposableTemplate:
"""Template that can include other templates."""
def __init__(
self,
name: str,
template: str,
components: dict[str, TemplateComponent] = None
):
self.name = name
self.template = template
self.components = components or {}
def add_component(self, component: TemplateComponent):
"""Add a reusable component."""
self.components[component.name] = component
def render(self, **kwargs) -> str:
"""Render template with components."""
# First, expand component references
expanded = self.template
for comp_name, component in self.components.items():
placeholder = f"{{{{component:{comp_name}}}}}"
expanded = expanded.replace(placeholder, component.content)
# Then render variables
return expanded.format(**kwargs)
# Example: Composable system prompt
PERSONA_COMPONENT = TemplateComponent(
name="persona",
content="""You are a helpful AI assistant with expertise in {domain}.
You communicate in a {tone} manner and always provide accurate information."""
)
SAFETY_COMPONENT = TemplateComponent(
name="safety",
content="""Important guidelines:
- Never provide harmful or dangerous information
- Acknowledge uncertainty when appropriate
- Recommend professional help for serious matters"""
)
OUTPUT_FORMAT_COMPONENT = TemplateComponent(
name="output_format",
content="""Format your response as follows:
- Use clear headings for different sections
- Include examples where helpful
- Keep explanations concise but complete"""
)
class TemplateBuilder:
"""Builder for constructing complex templates."""
def __init__(self, name: str):
self.name = name
self._parts: list[str] = []
self._variables: dict[str, dict] = {}
def add_section(self, title: str, content: str) -> "TemplateBuilder":
"""Add a section to the template."""
self._parts.append(f"## {title}\n{content}")
return self
def add_variable(
self,
name: str,
description: str = "",
required: bool = True,
default: Any = None
) -> "TemplateBuilder":
"""Define a variable."""
self._variables[name] = {
"description": description,
"required": required,
"default": default
}
return self
def add_component(self, component: TemplateComponent) -> "TemplateBuilder":
"""Add a component."""
self._parts.append(component.content)
return self
def build(self) -> PromptTemplate:
"""Build the final template."""
template_str = "\n\n".join(self._parts)
return PromptTemplate(
name=self.name,
template=template_str,
variables=self._variables
)
# Example usage
def build_assistant_template() -> PromptTemplate:
return (
TemplateBuilder("assistant")
.add_section("Role", "You are {role} specializing in {specialty}.")
.add_variable("role", "The assistant's role", required=True)
.add_variable("specialty", "Area of expertise", required=True)
.add_component(SAFETY_COMPONENT)
.add_section("Task", "{task}")
.add_variable("task", "The user's task", required=True)
.add_component(OUTPUT_FORMAT_COMPONENT)
.build()
)
Template Testing
from dataclasses import dataclass
from typing import Any, Optional, Callable
import re
@dataclass
class TestCase:
"""A test case for a template."""
name: str
variables: dict[str, Any]
expected_contains: list[str] = None
expected_not_contains: list[str] = None
expected_length_range: tuple[int, int] = None
@dataclass
class TestResult:
"""Result of a template test."""
test_name: str
passed: bool
rendered: str = None
errors: list[str] = None
class TemplateValidator:
"""Validate prompt templates."""
def validate_syntax(self, template: PromptTemplate) -> list[str]:
"""Validate template syntax."""
errors = []
# Check for balanced braces
open_count = template.template.count('{')
close_count = template.template.count('}')
if open_count != close_count:
errors.append(f"Unbalanced braces: {open_count} open, {close_count} close")
# Check that all variables are defined
used_vars = set(re.findall(r'\{(\w+)\}', template.template))
defined_vars = set(template.variables.keys())
undefined = used_vars - defined_vars
if undefined:
errors.append(f"Undefined variables: {undefined}")
unused = defined_vars - used_vars
if unused:
errors.append(f"Unused variables: {unused}")
return errors
def validate_rendering(
self,
template: PromptTemplate,
test_cases: list[TestCase]
) -> list[TestResult]:
"""Validate template rendering with test cases."""
results = []
for test in test_cases:
errors = []
rendered = None
try:
rendered = template.render(**test.variables)
# Check expected content
if test.expected_contains:
for expected in test.expected_contains:
if expected not in rendered:
errors.append(f"Missing expected content: {expected}")
if test.expected_not_contains:
for unexpected in test.expected_not_contains:
if unexpected in rendered:
errors.append(f"Contains unexpected content: {unexpected}")
# Check length
if test.expected_length_range:
min_len, max_len = test.expected_length_range
if not (min_len <= len(rendered) <= max_len):
errors.append(
f"Length {len(rendered)} outside range [{min_len}, {max_len}]"
)
except Exception as e:
errors.append(f"Rendering error: {str(e)}")
results.append(TestResult(
test_name=test.name,
passed=len(errors) == 0,
rendered=rendered,
errors=errors if errors else None
))
return results
class TemplateTestSuite:
"""Test suite for templates."""
def __init__(self, registry: TemplateRegistry):
self.registry = registry
self.validator = TemplateValidator()
self._test_cases: dict[str, list[TestCase]] = {}
def add_test(self, template_name: str, test: TestCase):
"""Add a test case for a template."""
if template_name not in self._test_cases:
self._test_cases[template_name] = []
self._test_cases[template_name].append(test)
def run_all(self) -> dict[str, list[TestResult]]:
"""Run all tests."""
results = {}
for template_name, tests in self._test_cases.items():
template = self.registry.get(template_name)
if template is None:
results[template_name] = [TestResult(
test_name="template_exists",
passed=False,
errors=["Template not found"]
)]
continue
# Syntax validation
syntax_errors = self.validator.validate_syntax(template)
if syntax_errors:
results[template_name] = [TestResult(
test_name="syntax",
passed=False,
errors=syntax_errors
)]
continue
# Run test cases
results[template_name] = self.validator.validate_rendering(
template, tests
)
return results
Production Template Service
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from typing import Optional
app = FastAPI()
# Initialize registry
registry = TemplateRegistry()
# Register default templates
registry.register(SUMMARIZATION_TEMPLATE)
registry.register(QA_TEMPLATE)
class CreateTemplateRequest(BaseModel):
name: str
template: str
description: str = ""
variables: dict = {}
version: str = "1.0.0"
tags: list[str] = []
class RenderRequest(BaseModel):
template_name: str
version: Optional[str] = None
variables: dict
@app.post("/v1/templates")
async def create_template(request: CreateTemplateRequest):
"""Create a new template."""
template = PromptTemplate(
name=request.name,
template=request.template,
description=request.description,
variables=request.variables,
version=request.version,
tags=request.tags
)
# Validate syntax
validator = TemplateValidator()
errors = validator.validate_syntax(template)
if errors:
raise HTTPException(status_code=400, detail={"errors": errors})
version = registry.register(template)
return {
"name": template.name,
"version": template.version,
"created_at": version.created_at.isoformat()
}
@app.get("/v1/templates")
async def list_templates():
"""List all templates."""
return {"templates": registry.list_templates()}
@app.get("/v1/templates/{name}")
async def get_template(name: str, version: Optional[str] = None):
"""Get a template by name."""
template = registry.get(name, version)
if template is None:
raise HTTPException(status_code=404, detail="Template not found")
return {
"name": template.name,
"template": template.template,
"description": template.description,
"variables": template.variables,
"version": template.version,
"tags": template.tags
}
@app.get("/v1/templates/{name}/versions")
async def get_versions(name: str):
"""Get all versions of a template."""
versions = registry.get_versions(name)
return {
"versions": [
{
"version": v.version,
"created_at": v.created_at.isoformat(),
"created_by": v.created_by,
"changelog": v.changelog,
"is_active": v.version == registry._active_versions.get(name)
}
for v in versions
]
}
@app.post("/v1/templates/{name}/activate/{version}")
async def activate_version(name: str, version: str):
"""Set the active version for a template."""
try:
registry.set_active(name, version)
return {"activated": True}
except ValueError as e:
raise HTTPException(status_code=404, detail=str(e))
@app.post("/v1/render")
async def render_template(request: RenderRequest):
"""Render a template with variables."""
try:
rendered = registry.render(
request.template_name,
request.version,
**request.variables
)
return {
"rendered": rendered,
"template_name": request.template_name,
"version": request.version or registry._active_versions.get(request.template_name)
}
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
@app.get("/health")
async def health():
return {"status": "healthy"}
References
- LangChain Prompts: https://python.langchain.com/docs/modules/model_io/prompts/
- Jinja2 Templates: https://jinja.palletsprojects.com/
- Prompt Engineering Guide: https://www.promptingguide.ai/
- OpenAI Prompt Engineering: https://platform.openai.com/docs/guides/prompt-engineering
Conclusion
Prompt template management brings engineering discipline to prompt development. Start with a clear template structure that separates static content from dynamic variables. Use a registry to centralize templates and enable versioning—this lets you roll back problematic changes and A/B test improvements. Build templates from reusable components for consistency across your application. Implement validation to catch syntax errors before they reach production. Test templates with representative inputs to ensure they render correctly. The key insight is that prompts are code—they deserve the same version control, testing, and deployment practices as your application code. A well-designed template system lets you iterate on prompts rapidly while maintaining reliability. As your LLM application grows, good template management becomes essential for maintaining quality and enabling collaboration across teams.
Discover more from Code, Cloud & Context
Subscribe to get the latest posts sent to your email.