How to build a custom prompt template library
Python environment, LLM access
What this does
A prompt template library provides versioned, reusable, and composable prompt definitions across an organization. This guide builds a Python library with string interpolation, template inheritance, schema validation, and versioning for production prompt management.
Steps
Step 1: Set up the project structure
mkdir prompt_library && cd prompt_library
pip install jinja2 pydantic
touch __init__.py prompt_templates.py template_manager.py
Step 2: Define the PromptTemplate class
from typing import Dict, List, Optional, Any
from dataclasses import dataclass, field
import json
@dataclass
class PromptTemplate:
"""Represents a versioned prompt template with inheritance support."""
name: str
template: str
version: str = "1.0.0"
variables: List[str] = field(default_factory=list)
parent_name: Optional[str] = None
schema: Optional[Dict] = None
def render(self, **kwargs) -> str:
"""Render the template by substituting variables using Jinja2-style syntax."""
from string import Template
import re
rendered = self.template
# Extract all {{variable}} patterns
found_vars = set(re.findall(r'\{\{(\w+)\}\}', self.template))
for var in found_vars:
if var not in kwargs:
raise ValueError(
f"Missing required variable '{var}' in template '{self.name}'. "
f"Expected: {list(found_vars)}"
)
rendered = rendered.replace(f"{{{{{var}}}}}", str(kwargs[var]))
# Handle any remaining {{variable}} placeholders
unrendered = re.findall(r'\{\{(\w+)\}\}', rendered)
if unrendered:
raise ValueError(f"Unrendered variables: {unrendered}")
return rendered
def validate(self, data: Dict) -> bool:
"""Validate rendered output or input data against JSON schema."""
if not self.schema:
return True
required = self.schema.get("required", [])
for key in required:
if key not in data:
raise ValueError(f"Schema validation failed: missing required field '{key}'")
return True
Step 3: Implement the PromptLibrary manager
class PromptLibrary:
"""Central registry for managing prompt templates with versioning."""
def __init__(self):
self._templates: Dict[str, List[PromptTemplate]] = {}
self._latest: Dict[str, PromptTemplate] = {}
def register(self, template: PromptTemplate):
"""Register a template. Raises ValueError if name+version already exists."""
if template.name not in self._templates:
self._templates[template.name] = []
existing_versions = [t.version for t in self._templates[template.name]]
if template.version in existing_versions:
raise ValueError(f"Template {template.name} v{template.version} already registered.")
self._templates[template.name].append(template)
self._templates[template.name].sort(key=lambda t: t.version)
self._latest[template.name] = self._templates[template.name][-1]
def get(self, name: str, version: Optional[str] = None) -> PromptTemplate:
"""Retrieve a template by name and optional version."""
if name not in self._templates:
raise KeyError(f"Template '{name}' not found.")
if version:
for t in self._templates[name]:
if t.version == version:
return t
raise KeyError(f"Version {version} not found for template '{name}'.")
return self._latest[name]
def render(self, name: str, variables: Dict, version: Optional[str] = None) -> Dict[str, Any]:
"""Render a template with variables and return metadata."""
template = self.get(name, version)
rendered = template.render(**variables)
return {
"rendered_prompt": rendered,
"template_name": template.name,
"version": template.version,
"variables_provided": list(variables.keys())
}
def list_templates(self) -> List[Dict]:
"""List all templates with their versions."""
return [
{"name": name, "versions": [t.version for t in versions]}
for name, versions in self._templates.items()
]
Step 4: Register and use templates
# Create the library instance
library = PromptLibrary()
# Register a document analysis template
doc_analysis = PromptTemplate(
name="document_analysis",
version="1.0.0",
template=(
"You are an expert document analyst.\n"
"Analyze the following document named {{filename}} ({{document_type}}).\n\n"
"Content:\n{{content}}\n\n"
"Provide: summary, key entities, and classification."
),
variables=["filename", "document_type", "content"],
schema={"required": ["filename", "content"]}
)
library.register(doc_analysis)
# Register an extended version with new variables
doc_analysis_v2 = PromptTemplate(
name="document_analysis",
version="2.0.0",
template=(
"You are an expert document analyst specializing in {{domain}}.\n"
"Analyze the following document named {{filename}} ({{document_type}}).\n\n"
"Content:\n{{content}}\n\n"
"Confidence threshold: {{confidence_threshold}}\n"
"Provide: summary, key entities, classification, and risk assessment."
),
variables=["filename", "document_type", "content", "domain", "confidence_threshold"],
parent_name="document_analysis",
schema={"required": ["filename", "content", "domain"]}
)
library.register(doc_analysis_v2)
# Register a customer support template
support_template = PromptTemplate(
name="customer_support",
version="1.0.0",
template=(
"Customer ID: {{customer_id}}\n"
"Issue: {{issue_description}}\n"
"Priority: {{priority}}\n"
"Response format: {{response_format}}\n"
"{{additional_context}}"
),
variables=["customer_id", "issue_description", "priority", "response_format", "additional_context"]
)
library.register(support_template)
# Render a template
result = library.render(
"document_analysis",
variables={
"filename": "Q4_report.pdf",
"document_type": "financial report",
"content": "Revenue increased by 15% year-over-year..."
}
)
print(result)
Step 5: Verify expected output
Expected output from print(result):
{
"rendered_prompt": "You are an expert document analyst.\nAnalyze the following document named Q4_report.pdf (financial report).\n\nContent:\nRevenue increased by 15% year-over-year...\n\nProvide: summary, key entities, and classification.",
"template_name": "document_analysis",
"version": "1.0.0",
"variables_provided": ["filename", "document_type", "content"]
}
Verify: rendered_prompt contains all substituted variables (no {{...}} remaining), template_name matches the registered name, version is the latest registered version.
To retrieve a specific version:
v1 = library.get("document_analysis", version="1.0.0")
v2 = library.get("document_analysis", version="2.0.0")
print(v1.version, v2.version) # 1.0.0 2.0.0
Verification
Common failures
Double curly braces left in output. If
render()receives fewer variables than the template has placeholders, unmatched{{var}}strings remain in the output. Always validate variable completeness before callingrender().Version collision on re-registration. Attempting to register the same name+version twice raises an error. Use a version bump strategy: increment the minor version (1.0.0 -> 1.1.0) when updating a template.
No inheritance resolution between parent templates. The
parent_namefield is stored but not automatically merged inrender(). Implement inheritance by fetching the parent template and prepending its content in therender()method for templates with a non-nullparent_name.
- Version mismatch - The installed package or runtime differs from the command shown; check the version first and rerun the smallest verification command.
- Local environment drift - Another service, virtual environment, model, or path is being used; print the active binary path and configuration before changing the guide steps.
Related guides
- Build a Document Analysis Agent Pipeline - Use the prompt library to standardize extraction and summarization prompts across document types in the pipeline.
- Implement Few-Shot Example Selection Dynamically - Extend template rendering to inject dynamically selected few-shot examples as a variable, building on the library's variable interpolation system.