Template
User-defined prompt template engine for LLM applications
Classes
| Class | Description |
|---|---|
| PromptTemplate | Prompt template with Jinja2 syntax for AI/LLM workflows. |
| TemplateEnvironment | Shared configuration for a group of templates. |
| DebugResult | Result of rendering with debug mode enabled. |
| DebugSpan | Span in rendered template output for debug visualization. |
| ValidationResult | Result of template input validation. |
class PromptTemplate
PromptTemplate(
self,
source: str,
strict: bool = True
)
self,
source: str,
strict: bool = True
)
Prompt template with Jinja2 syntax for AI/LLM workflows.
A PromptTemplate compiles once and can be rendered many times with different variables. Supports standard Jinja2 syntax including control flow, filters, and functions - enabling seamless migration from existing Jinja2 templates.
Features for Prompt Engineering
Introspection - Discover required variables without running:
>>> t = PromptTemplate("Hello {{ name }}, you are {{ age }} years old")
>>> t.input_variables
{'name', 'age'}
Partial Application - Pre-fill variables for pipelines:
>>> system = PromptTemplate("System: {{ persona }}\\nUser: {{ query }}")
>>> chat = system.partial(persona="You are a helpful assistant")
>>> chat(query="Hello!") # Only need to provide query
'System: You are a helpful assistant\nUser: Hello!'
Custom Filters - Register Python functions as template filters:
>>> t = PromptTemplate("{{ name | shout }}")
>>> t.register_filter("shout", lambda s: s.upper() + "!!!")
>>> t(name="hello")
'HELLO!!!'
Chat Formatting
PromptTemplate includes presets for common chat formats:
>>> t = PromptTemplate.chatml()
>>> prompt = t.apply([
... {"role": "user", "content": "Hello!"},
... ])
Available presets: chatml(), llama2(), alpaca(), vicuna(), zephyr()
Basic Usage
>>> from talu import PromptTemplate
# Create template >>> t = PromptTemplate("Hello {{ name }}!")
# Render with variables (three equivalent ways) >>> t(name="World") # Callable (recommended) 'Hello World!' >>> t.format(name="World") # Like str.format() 'Hello World!' >>> t.render(name="World") # Like Jinja2 'Hello World!'
# RAG example >>> rag = PromptTemplate(''' ... Context: ... {% for doc in docs %} ... - {{ doc.content }} ... {% endfor %} ... ... Question: {{ question }} ... ''') >>> rag(docs=[{"content": "Paris is in France."}], ... question="Where is Paris?")
Supported Jinja2 Features
- Fully Supported:
- Variables:
{{ name }},{{ user.name }} - Control flow:
{% if %},{% for %},{% set %},{% macro %} - Filters:
| upper,| lower,| join,| default, etc. - Operators:
+,-,*,/,in,not in,is,and,or - Functions:
range(),dict(),namespace() - Comments:
{# comment #} - Whitespace control:
{{- name -}},{%- if -%} - Custom Python filters via
register_filter() - Template composition via
{% include %}(see below)
Template Composition: The {% include %} tag allows including other templates dynamically. The argument is an expression that evaluates to a template string at runtime - it does not load from the filesystem:
>>> t = PromptTemplate("{% include header %}Body")
>>> t(header="=== {{ title }} ===", title="My Doc")
'=== My Doc ===Body'
This design differs from standard Jinja2 (which loads files) for
security reasons - templates cannot access the filesystem. Pass
template strings as variables instead.
Included templates have access to the parent template's context,
including macros defined in previously included templates:
>>> t = PromptTemplate("{% include utils %}{{ greet('World') }}")
>>> t(utils="{% macro greet(n) %}Hello {{ n }}!{% endmacro %}")
'Hello World!'
- Not Supported:
- Template inheritance (
{% extends %},{% block %}) - use{% include %}for composition instead - File-based includes - pass template strings as variables for security
Parameters
sourceThe template string with Jinja2 syntax.
Raises
TemplateSyntaxErrorIf the template has invalid syntax.
Quick Reference
Properties
| Name | Type |
|---|---|
input_variables |
set[str] |
source |
str |
strict |
bool |
supports_system_role |
bool |
supports_tools |
bool |
Methods
| Method | Description |
|---|---|
__call__() |
Render the template with the given variables. |
apply() |
Render chat messages using this template. |
format() |
Render the template with the given variables. |
from_chat_template() |
Load a model's chat template as an inspectable ... |
from_file() |
Load a template from a file. |
from_preset() |
Create a PromptTemplate from a built-in chat fo... |
partial() |
Return a new PromptTemplate with some variables... |
register_filter() |
Register a custom Python filter function. |
render() |
Render the template with the given variables. |
validate() |
Validate inputs and prepare for efficient rende... |
Properties
input_variables: set[str]
Return the set of variable names required by this template.
This enables building tools on top of templates (CLIs, web UIs, validators) that need to know what inputs are expected without executing the template.
Returns
Set of variable names found in the template source.
This uses AST-based extraction via the Zig template engine, which accurately detects all required variables including:
- Simple variables:
{{ name }} - Attribute access:
{{ user.name }}(extractsuser) - Array indexing:
{{ items[0] }}(extractsitems) - Dictionary access:
{{ data['key'] }}(extractsdata) - Complex expressions:
{{ a if b else c }} - Nested in filters:
{{ name | default(fallback) }} - In conditions:
{% if show %} - In loops (the iterable):
{% for x in items %}
- Automatically excluded:
- Loop iteration variables:
xin{% for x in items %} - Set variables:
xin{% set x = ... %} - Macro parameters
- Built-ins:
loop,true,false,none,range, etc.
Example
>>> t = PromptTemplate("Hello {{ name }}, you are {{ age }} years old")
>>> t.input_variables
{'name', 'age'}
>>> rag = PromptTemplate('''
... {% for doc in docs %}
... {{ doc.content }}
... {% endfor %}
... Question: {{ question }}
... ''')
>>> rag.input_variables
{'docs', 'question'}
source: str
The original template source string.
strict: bool
Whether strict mode is enabled (undefined variables raise errors).
supports_system_role: bool
Check if this chat template explicitly handles the 'system' role.
Returns True if the template source contains explicit handling for system messages (e.g., message.role == 'system' or role == "system").
- This helps Chat implementations decide whether to:
- Pass system messages directly (if supported)
- Prepend system content to the first user message (if not supported)
Returns
True if template handles system role, False otherwise.
Example
>>> t = PromptTemplate.chatml()
>>> t.supports_system_role # ChatML handles any role
True
>>> t = PromptTemplate("{{ messages[0].content }}")
>>> t.supports_system_role # No role handling
False
This is a heuristic based on source inspection. Templates that use {{ message.role }} directly (like ChatML) will return True because they handle any role generically.
supports_tools: bool
Check if this chat template has built-in tool/function calling support.
Returns True if the template source contains handling for tools, functions, or available_tools variables (common in HuggingFace templates for models like Llama-3, Qwen, etc.).
- This helps Chat implementations decide whether to:
- Let the template handle tool formatting (if supported)
- Handle tool formatting separately (if not supported)
When a template has built-in tool support but the Chat module also handles tools, pass an empty tools list to the template to avoid conflicts.
Returns
True if template has tool handling, False otherwise.
Example
>>> t = PromptTemplate.chatml()
>>> t.supports_tools # Basic ChatML has no tool handling
False
This is a heuristic. Templates with tool support typically check for tools, available_tools, or functions variables.
Methods
def __call__(
self,
debug: bool = False,
strict: bool | None = None,
variables: Any
) → str | DebugResult
self,
debug: bool = False,
strict: bool | None = None,
variables: Any
) → str | DebugResult
Render the template with the given variables.
Parameters
debugIf True, return DebugResult with span tracking.
strictOverride strict mode for this render only. **variables: Variables to substitute in the template.
Returns
Rendered string, or DebugResult if debug=True.
Example
>>> t = PromptTemplate("Hello {{ name }}!")
>>> t(name="World")
'Hello World!'
def apply(
self,
messages: list[dict[str, Any]],
debug: bool = False,
strict: bool | None = None,
add_generation_prompt: bool = True,
bos_token: str = '',
eos_token: str = '',
kwargs: Any
) → str | DebugResult
self,
messages: list[dict[str, Any]],
debug: bool = False,
strict: bool | None = None,
add_generation_prompt: bool = True,
bos_token: str = '',
eos_token: str = '',
kwargs: Any
) → str | DebugResult
Render chat messages using this template.
Convenience wrapper around __call__() with named parameters for common chat template variables.
Parameters
messagesList of message dicts with
roleandcontentkeys.debugIf True, return DebugResult with span tracking.
strictOverride strict mode for this render only.
add_generation_promptAdd assistant marker at end (default True).
bos_tokenBeginning of sequence token (model-specific).
eos_tokenEnd of sequence token (model-specific). **kwargs: Additional template variables (e.g.,
tools).
Returns
Formatted prompt string, or DebugResult if debug=True.
Example
>>> t = PromptTemplate.chatml()
>>> prompt = t.apply([
... {"role": "system", "content": "You are helpful."},
... {"role": "user", "content": "Hello!"},
... ])
def format(self, variables: Any) → str
Render the template with the given variables.
Familiar API for str.format() users.
Parameters
**variables: Variables to substitute in the template.
Returns
The rendered template string.
Example
>>> t = PromptTemplate("Hello {{ name }}!")
>>> t.format(name="World")
'Hello World!'
def from_chat_template(
cls,
model: str,
strict: bool = False
) → PromptTemplate
cls,
model: str,
strict: bool = False
) → PromptTemplate
Load a model's chat template as an inspectable PromptTemplate.
Parameters
modelModel path or HuggingFace ID (e.g., "Qwen/Qwen3-0.6B").
strictIf True, undefined variables raise errors.
Returns
PromptTemplate with the model's chat template.
Raises
FileNotFoundErrorIf model not found or has no chat template.
Example
>>> t = PromptTemplate.from_chat_template("Qwen/Qwen3-0.6B")
>>> t.input_variables
{'messages', 'add_generation_prompt', ...}
def from_file(cls, path: str) → PromptTemplate
Load a template from a file.
Parameters
pathPath to the template file.
Returns
A new PromptTemplate instance.
Raises
FileNotFoundErrorIf the file doesn't exist.
TemplateSyntaxErrorIf template syntax is invalid.
Example
>>> template = PromptTemplate.from_file("prompts/rag.j2")
>>> result = template(documents=docs, question="...")
def from_preset(cls, name: str) → PromptTemplate
Create a PromptTemplate from a built-in chat format preset.
Available presets: chatml, llama2, alpaca, vicuna, zephyr.
Parameters
namePreset name (case-sensitive).
Returns
PromptTemplate configured for the specified format.
Raises
ValueErrorIf name is not a known preset.
Example
>>> t = PromptTemplate.from_preset("chatml")
>>> t.apply([{"role": "user", "content": "Hi"}])
'<|im_start|>user\\nHi<|im_end|>\\n<|im_start|>assistant\\n'
def partial(self, kwargs: Any) → PromptTemplate
Return a new PromptTemplate with some variables pre-filled.
Parameters
**kwargs: Variables to pre-fill in the new template.
Returns
A new PromptTemplate with the variables baked in.
Example
>>> t = PromptTemplate("{{ persona }}\\n{{ query }}")
>>> chat = t.partial(persona="You are helpful")
>>> chat(query="Hello!")
'You are helpful\nHello!'
def register_filter(
self,
name: str,
func: Any
) → PromptTemplate
self,
name: str,
func: Any
) → PromptTemplate
Register a custom Python filter function.
Parameters
nameFilter name to use in templates (e.g.,
{{ x | name }}).funcCallable that takes the piped value and returns a result.
Returns
Self, for method chaining.
Example
>>> t = PromptTemplate("{{ name | shout }}")
>>> t.register_filter("shout", lambda s: s.upper() + "!")
>>> t(name="hello")
'HELLO!'
def render(
self,
strict: bool | None = None,
variables: Any
) → str
self,
strict: bool | None = None,
variables: Any
) → str
Render the template with the given variables.
Familiar API for Jinja2 users.
Parameters
strictOverride the instance's strict mode for this render only. If None (default), uses the instance's strict setting. **variables: Variables to substitute in the template.
Returns
The rendered template string.
Example
>>> t = PromptTemplate("Hello {{ name }}!")
>>> t.render(name="World")
'Hello World!'
def validate(self, kwargs: Any) → ValidationResult
Validate inputs and prepare for efficient rendering.
Parameters
**kwargs: Variables to validate against template requirements.
Returns
ValidationResult with is_valid, required, optional, extra. Use result.render() to render without re-serializing variables.
Example
>>> t = PromptTemplate("Hello {{ name }}, age {{ age }}")
>>> result = t.validate(name="Alice")
>>> result.is_valid
False
>>> result.required
{'age'}
class TemplateEnvironment
TemplateEnvironment(self, strict: bool = True)
Shared configuration for a group of templates.
Use an environment when you have multiple templates that share:
- Custom filters: Date formatting, text processing, domain logic
- Global variables: App name, version, feature flags, constants
- Default settings: Strict mode, undefined behavior
This eliminates boilerplate when you have many templates that need the same configuration.
Parameters
strictDefault strict mode for templates. When True (default), undefined variables raise errors instead of rendering as empty. This prevents silent failures in LLM prompts.
Example
Set up an environment for your application:
>>> from talu import TemplateEnvironment
>>> env = TemplateEnvironment() # strict=True by default
>>> env.globals["app_name"] = "MyAssistant"
>>> env.globals["version"] = "2.0"
>>> env.register_filter("currency", lambda x: f"${x:,.2f}")
Create templates from the environment::
>>> greeting = env.from_string("Welcome to {{ app_name }} v{{ version }}!")
>>> greeting()
'Welcome to MyAssistant v2.0!'
>>> invoice = env.from_string("Total: {{ amount | currency }}")
>>> invoice(amount=99.50)
'Total: $99.50'
Load templates from files::
>>> prompt = env.from_file("prompts/rag_context.j2")
See Also -------- PromptTemplate : For standalone templates without shared config.
Quick Reference
Properties
| Name | Type |
|---|---|
filters |
dict[str, Callable] |
globals |
dict[str, Any] |
strict |
bool |
Methods
| Method | Description |
|---|---|
from_file() |
Load a template from a file with this environme... |
from_string() |
Create a template from a string with this envir... |
register_filter() |
Register a filter for this environment. |
Properties
filters: dict[str, Callable]
Custom filters for this environment.
Modify directly to add/remove filters, or use register_filter().
Example
>>> env.filters["shout"] = lambda s: s.upper() + "!"
globals: dict[str, Any]
Global variables available to all templates in this environment.
These are merged with render-time variables, with render-time variables taking precedence.
Example
>>> env.globals["app_name"] = "MyApp"
>>> env.globals["debug"] = True
strict: bool
Default strict mode for templates created from this environment.
Methods
def from_file(
self,
path: str | Path,
strict: bool | None = None
) → PromptTemplate
self,
path: str | Path,
strict: bool | None = None
) → PromptTemplate
Load a template from a file with this environment's configuration.
Parameters
pathPath to template file.
strictOverride the environment's default strict mode.
Returns
A PromptTemplate with this environment's filters and globals.
Raises
FileNotFoundErrorIf the template file doesn't exist.
Example
>>> t = env.from_file("prompts/greeting.j2")
def from_string(
self,
source: str,
strict: bool | None = None
) → PromptTemplate
self,
source: str,
strict: bool | None = None
) → PromptTemplate
Create a template from a string with this environment's configuration.
Parameters
sourceTemplate source string (Jinja2 syntax).
strictOverride the environment's default strict mode.
Returns
A PromptTemplate with this environment's filters and globals.
Example
>>> t = env.from_string("Hello {{ name | upper }}!")
>>> t(name="world")
'Hello WORLD!'
def register_filter(
self,
name: str,
func: Callable
) → TemplateEnvironment
self,
name: str,
func: Callable
) → TemplateEnvironment
Register a filter for this environment.
Parameters
nameFilter name to use in templates.
funcFilter function.
Returns
Self, for method chaining.
Raises
ValidationErrorIf func is not callable.
Example
>>> env.register_filter("upper", str.upper).register_filter("trim", str.strip)
class DebugResult
DebugResult(
self,
output: str,
spans: list[DebugSpan]
)
self,
output: str,
spans: list[DebugSpan]
)
Result of rendering with debug mode enabled.
Contains the rendered output plus detailed span information showing which parts came from variables vs static text.
Attributes
outputThe rendered template string.
spansList of DebugSpan showing the source of each part.
Quick Reference
Methods
| Method | Description |
|---|---|
format_ansi() |
Format output with ANSI colors for terminal dis... |
format_plain() |
Format output with plain text markers (no ANSI). |
Methods
def format_ansi(self) → str
Format output with ANSI colors for terminal display.
- Static text: normal (no color)
- Variables: cyan with variable name in parentheses
- Expressions: yellow
Empty variable values produce no output, so they don't appear in the span list. Use PromptTemplate.validate() before rendering to detect empty or missing values.
Returns
Formatted string with ANSI escape codes.
def format_plain(self) → str
Format output with plain text markers (no ANSI).
- Static text: unchanged
- Variables: «value» (var_name)
- Expressions: «value»
Empty variable values produce no output, so they don't appear in the span list. Use PromptTemplate.validate() before rendering to detect empty or missing values.
Returns
Formatted string with plain text markers.
class DebugSpan
DebugSpan(
self,
start: int,
end: int,
source: str,
text: str
)
self,
start: int,
end: int,
source: str,
text: str
)
Span in rendered template output for debug visualization.
Tracks which parts of the output came from variables vs static template text. This enables debugging "why did the model hallucinate?" by showing exactly which variable produced which part of the output.
Attributes
startStart position in output (inclusive).
endEnd position in output (exclusive).
sourceWhat produced this span - "static", variable path (e.g., "name", "user.email"), or "expression".
textThe actual text content of this span.
Quick Reference
Properties
| Name | Type |
|---|---|
is_expression |
bool |
is_static |
bool |
is_variable |
bool |
Properties
is_expression: bool
Whether this span came from a complex expression.
is_static: bool
Whether this span is static template text.
is_variable: bool
Whether this span came from a variable substitution.
class ValidationResult
ValidationResult(
self,
required: set[str] = ,
optional: set[str] = ,
extra: set[str] = ,
invalid: dict[str, str] = ,
_template: Any = None,
_json_vars: str | None = None,
_strict: bool = False
)
self,
required: set[str] =
optional: set[str] =
extra: set[str] =
invalid: dict[str, str] =
_template: Any = None,
_json_vars: str | None = None,
_strict: bool = False
)
Result of template input validation.
Distinguishes between required and optional variables. Required variables are used "naked" in the template ({{ name }}), while optional variables have a default() filter ({{ context | default('') }}).
This distinction helps when loading third-party templates via PromptTemplate.from_chat_template() - you know which variables will break the template vs which will safely fall back to defaults.
The ValidationResult can also render the template directly, avoiding re-serialization of the variables that were already validated:
result = template.validate(documents=large_docs)
if result.is_valid:
output = result.render() # No re-serialization!
Attributes
requiredVariables required by the template (no default) but not provided. Missing these will likely break template logic or produce empty output.
optionalVariables with default() filter that are not provided. These are safe to omit - the template handles missing values gracefully.
extraVariables provided but not used by the template. These are warnings, not errors - extra variables are ignored.
invalidDictionary mapping variable names to error messages for values that cannot be serialized to JSON.
Example
>>> t = PromptTemplate("Hello {{ name }}! Context: {{ context | default('N/A') }}")
>>> result = t.validate(name="Alice")
>>> result.is_valid
True # Only 'required' matters for validity
>>> result.required
set() # name was provided
>>> result.optional
{'context'} # context has default, safe to omit
# Efficient validate-then-render pattern: >>> result = t.validate(name="Alice") >>> if result: ... output = result.render() # Reuses serialized variables
Quick Reference
Properties
| Name | Type |
|---|---|
is_valid |
bool |
summary |
str |
Methods
| Method | Description |
|---|---|
render() |
Render the template using the validated variables. |
Properties
is_valid: bool
Whether validation passed.
Returns True only if there are no missing required variables and no invalid (non-JSON-serializable) values.
Optional and extra variables do NOT cause validation to fail. Only required variables and invalid values matter for validity.
summary: str
Human-readable summary of validation issues.
Returns empty string if validation passed.
Example
>>> result.summary
'Missing required variables: name. Missing optional variables (have defaults): context'
Methods
def render(self, strict: bool | None = None) → str
Render the template using the validated variables.
This method reuses the JSON serialization from validation, avoiding the overhead of re-serializing large variables like RAG documents.
Parameters
strictOverride strict mode for this render only. If None (default), uses the mode from validation.
Returns
The rendered template string.
Raises
ValueErrorIf validation failed or result wasn't created by validate().
TemplateErrorIf rendering fails.
Example
Efficient validate-then-render for large documents:
>>> docs = load_large_documents() # 10MB of data
>>> result = template.validate(documents=docs, question="What is...?")
>>> if result.is_valid:
... output = result.render() # No re-serialization!
With strict mode override:
>>> result = template.validate(name="Alice")
>>> output = result.render(strict=True) # Force strict for this render