t-prompts Tutorial¶
This tutorial covers all major features of t-prompts: a library for creating structured prompts using Python 3.14's template strings (t-strings) that preserve provenance while rendering to text.
Each section introduces a concept with a working example that you can run and interact with.
0. Setup & Imports¶
To enable interactive widget rendering in Jupyter notebooks, call setup_notebook(). This displays structured prompts with an interactive visualization showing their internal structure, source mapping, and more.
The function returns a widget that you should display to activate the rendering.
from PIL import Image
from t_prompts import dedent, prompt, setup_notebook
# Enable interactive rendering - display the returned widget
display(setup_notebook())
1. Hello World - Text Interpolation¶
Python 3.14's t-strings look like f-strings but return a Template object that preserves information about interpolations instead of immediately evaluating to a string.
The prompt() function wraps a t-string into a StructuredPrompt that acts like both a string and a navigable tree.
name = "Alice"
greeting = "Hello"
prompt(t"{greeting}, {name}! Welcome to t-prompts.")
2. Format Specs as Keys¶
The format spec (the part after : in an interpolation) becomes the key for accessing that interpolation. Without a format spec, the expression itself is used as the key.
Access interpolations using dictionary-like syntax: p["key"]
user_query = "What is Python?"
context = "Technical documentation"
p = prompt(t"Context: {context:ctx}. Question: {user_query:query}")
# Access interpolations by their keys
print(f"Keys: {list(p.keys())}")
print(f"Context value: {p['ctx'].value}")
print(f"Query expression: {p['query'].expression}")
p
Keys: ['ctx', 'query'] Context value: Technical documentation Query expression: user_query
3. Dedent for Multi-line Prompts¶
The dedent() function allows you to write clean, properly indented source code while producing output without that indentation. It's essential for readable multi-line prompts in functions and classes.
system_message = "You are a helpful assistant."
task_description = "Translate the following text to French."
user_input = "Hello, how are you?"
dedent(t"""
System: {system_message:system}
Task: {task_description:task}
User: {user_input:input}
""")
Segway: Output Widget Interactivity¶
How to use this widget
- Switch view modes: Use the toolbar toggles to switch between Code, Markdown, or Split views.
- Code view
- Collapse selections: Select text and press the space bar to collapse.
- Expand collapsed content: Double-click a collapsed segment or double-tap the space bar when nothing is selected to expand all.
- Tree view
- Single-click rows: Open or close tree nodes to inspect nested elements. The arrow button performs the same action.
- Double-click rows: Toggle the matching content in the code view—collapse visible sections or restore previously collapsed ones.
- Hide the panel: Use the « button in the tree header to tuck the tree away. Click the side strip to show it again.
If you are running this notebook locally, Cmd(or Ctrl)-Click to navigate to prompt source location in VSCode
4. Nested Composition¶
Build larger prompts from smaller StructuredPrompt pieces. This enables clean composition and reusable prompt components.
# Create reusable components
system = prompt(t"You are a helpful coding assistant.")
context = dedent(t"""
Language: Python
Framework: FastAPI
""")
question = prompt(t"How do I create a POST endpoint?")
# Compose them together
dedent(t"""
{system:system}
{context:context}
Question: {question:question}
""")
5. XML-Style Tags¶
You can use XML-style tags to structure your prompts, which is common in many LLM prompting patterns. The interpolations work seamlessly within the tags.
system_instructions = "You are a precise technical writer."
user_context = "Writing API documentation"
user_message = "Explain REST principles"
dedent(t"""
{system_instructions:instructions:xml=important}
- Context: {user_context:context}
- Message: {user_message:message}
""")
6. Markdown Headers with Dynamic Format Specs¶
Use the header= render hint to automatically create markdown sections. The library will render the content with the appropriate header level.
Dynamic format specs allow you to use interpolated expressions in the format spec itself, enabling programmatic key generation.
section_title = "Introduction"
section_content = "This section introduces the core concepts."
# The format spec {section_title}:header={section_title} uses dynamic interpolation
# The key becomes the value of section_title, and header= sets the header text
dedent(t"""
{section_content:{section_title}:header={section_title}}
""")
7. Nested Markdown Headers¶
The library automatically tracks header depth when nesting sections. Each nested level increments the header level (#, ##, ###, etc.).
# Helper function for creating sections
def section(title: str, content):
return dedent(t"""
{content:{title}:header={title}}
""")
# Create nested sections
subsection = section("Installation", "Run: pip install t-prompts")
main_section = section(
"Getting Started",
dedent(t"""
Welcome to the guide.
{subsection:subsection}
"""),
)
section(
"Tutorial",
dedent(t"""
This is a comprehensive tutorial.
{main_section:main}
"""),
)
8. Image Interpolation¶
Interpolate PIL Image objects directly into prompts. Hover over the text place holder to get a preview.
# Create a simple test image
img = Image.new("RGB", (100, 100), color="blue")
description = "Here is a blue square"
dedent(t"""
{description:description}
{img:image}
This demonstrates image interpolation with custom sizing.
""")
9. LaTeX Math¶
Display mathematical equations using LaTeX syntax with $$ delimiters. Interpolations work within LaTeX expressions. Interpolation means subterms can be collapsed: x^2 + [2 chars] + [2 chars] = 0 -- Try it!
# Helper for LaTeX blocks
def latex_block(equation):
return dedent(t"""
$$
{equation:equation}
$$
""")
term_a = "x^2"
term_b = "2x"
term_c = "1"
equation = prompt(t"{term_a:a} + {term_b:b} + {term_c:c} = 0")
dedent(t"""
Solve the quadratic equation:
{latex_block(equation):equation_display}
Using the quadratic formula.
""")
10. Code Blocks¶
Create fenced code blocks with structured, interpolated code pieces. This is useful for generating code examples with variable parts.
# Helper for code blocks
def code_block(language: str, code):
return dedent(t"""
```{language}
{code:code}
```
""")
function_name = "calculate_total"
param_name = "items"
operation = "sum(item.price for item in items)"
code = dedent(t"""
def {function_name:fn_name}({param_name:param}):
return {operation:calc}
""")
dedent(t"""
Here's a function to calculate totals:
{code_block("python", code):code_example}
""")
11. List Interpolations¶
Interpolate lists of StructuredPrompt objects with custom separators using sep= in the format spec. Default separator is newline.
# Create a list of bullet items
features = ["Fast performance", "Type safety", "Great DX"]
feature_prompts = [prompt(t"- {features[i]:feature_{str(i)}}") for i in range(len(features))]
# Also demonstrate custom separator
tags = ["python", "prompts", "llm"]
tag_prompts = [prompt(t"{tags[i]:tag_{str(i)}}") for i in range(len(tags))]
dedent(t"""
Key Features:
{feature_prompts:features}
Tags: {tag_prompts:tags:sep=, }
""")
12. Tables¶
Create markdown tables with interpolated cells, rows, or multi-row data. Tables are just text, but interpolations let you track provenance of each value.
# Single cell interpolation
metric_name = "Accuracy"
metric_value = "94.5%"
# Multi-row data
data_rows = [
("Precision", "92.1%"),
("Recall", "91.8%"),
("F1 Score", "91.9%"),
]
# Build rows with interpolations
row_prompts = [
prompt(t"| {data_rows[i][0]:metric_{str(i)}} | {data_rows[i][1]:value_{str(i)}} |") for i in range(len(data_rows))
]
dedent(t"""
## Model Metrics
| Metric | Value |
|--------|-------|
| {metric_name:metric} | {metric_value:value} |
{row_prompts:additional_rows}
""")
13. Navigation & Introspection¶
StructuredPrompt implements the mapping protocol, allowing dictionary-like access to interpolations. Navigate nested structures with chained subscripts.
# Create a nested structure
inner = prompt(t"Inner value: {'data':inner_data}")
outer = dedent(t"""
Outer context: {"context":outer_context}
{inner:inner_prompt}
""")
# Navigation examples
print(f"Top-level keys: {list(outer.keys())}")
print(f"Outer context: {outer['outer_context'].value}")
print(f"Inner prompt keys: {list(outer['inner_prompt'].keys())}")
print(f"Nested access: {outer['inner_prompt']['inner_data'].value}")
# Mapping protocol
print("\nIterating with .items():")
for key, node in outer.items():
print(f" {key}: {type(node).__name__}")
outer
Top-level keys: ['outer_context', 'inner_prompt'] Outer context: context Inner prompt keys: ['inner_data'] Nested access: data Iterating with .items(): outer_context: TextInterpolation inner_prompt: StructuredPrompt
14. Source Code Tracking¶
t-prompts preserves provenance: the original expressions, values, and source locations of every interpolation. This enables powerful debugging and introspection.
api_key = "sk-1234567890"
endpoint = "https://api.example.com"
p = prompt(t"API: {endpoint:endpoint}, Key: {api_key:key}")
# Access provenance information
endpoint_node = p["endpoint"]
print(f"Expression: {endpoint_node.expression}")
print(f"Value: {endpoint_node.value}")
print(f"Key: {endpoint_node.key}")
# The template preserves the original structure
print(f"\nTemplate strings: {p.template.strings}")
print(f"Template interpolations: {[interp.expression for interp in p.template.interpolations]}")
# Source information
print(f"\nSource: {p.creation_location}")
p
Expression: endpoint
Value: https://api.example.com
Key: endpoint
Template strings: ('API: ', ', Key: ', '')
Template interpolations: ['endpoint', 'api_key']
Source: SourceLocation(filename='803841533.py', filepath='/tmp/ipykernel_2357/803841533.py', line=4)
15. JSON Export¶
Export the entire prompt structure to JSON using toJSON(). This creates a serializable representation of the prompt including all nested structures and metadata.
import json
# Create a structured prompt
task = "translation"
language = "French"
inner = prompt(t"Target: {language:lang}")
p = dedent(t"""
Task: {task:task}
{inner:config}
""")
# Export to JSON
json_data = p.toJSON()
jsons = json.dumps(json_data)
print(f"{jsons[:40]} ... {jsons[-40:]}")
p
{"prompt_id": "a0437ff9-2158-41ae-9925-7 ... 00312579.py", "line": 8}, "value": ""}]}
16. Rendered Diffs¶
RenderedDiff shows text-level differences between two prompts. It compares the final rendered text and highlights what changed.
from t_prompts import diff_rendered_prompts
# Original prompt
version = "1.0"
feature = "basic auth"
old_prompt = dedent(t"""
Version: {version:version}
Feature: {feature:feature}
Status: In development
""")
# Updated prompt
version = "2.0"
feature = "OAuth2"
new_prompt = dedent(t"""
Version: {version:version}
Feature: {feature:feature}
Status: Released
""")
# Create and display the diff
diff_rendered_prompts(old_prompt, new_prompt)
17. Structural Diffs¶
StructuralDiff compares the structure and interpolations of two prompts, showing what changed at the semantic level rather than just text level.
from t_prompts.diff import diff_structured_prompts
# Create two prompts with different structures
model_v1 = "GPT-3"
temp_v1 = "0.7"
old = dedent(t"""
Model: {model_v1:model}
Temperature: {temp_v1:temperature}
""")
model_v2 = "GPT-4"
temp_v2 = "0.8"
max_tokens = "2048"
new = dedent(t"""
Model: {model_v2:model}
Temperature: {temp_v2:temperature}
Max Tokens: {max_tokens:max_tokens}
""")
# Create and display the structural diff
diff_structured_prompts(old, new)
18. Understanding IR (Intermediate Representation)¶
The IR (Intermediate Representation) is the linear sequence of text/image chunks that t-prompts builds from your template string. To make a call to an LLM, you would assemble these chunks into a message(s) (multimodal scenarios will typically interleave text and image messages)
You can access the IR to inspect the internal structure.
content = "Hello"
name = "World"
p = prompt(t"{content:greeting}, {name:target}!")
# Access the IR
ir = p.ir()
print(f"IR type: {type(ir).__name__}")
print(f"IR chunks: {len(ir._chunks)}")
print("\nIR structure:")
for i, chunk in enumerate(ir._chunks):
print(f" Chunk {i}: {type(chunk).__name__}")
print(f" Id: {chunk.id}")
IR type: IntermediateRepresentation IR chunks: 4 IR structure: Chunk 0: TextChunk Id: 82e09d0d-4684-440d-9d9e-b15889a5a58d Chunk 1: TextChunk Id: 03fc8654-418b-4df2-9ae9-6a335470fcbb Chunk 2: TextChunk Id: ec4d143f-f447-4d16-be7d-a0839f5440bd Chunk 3: TextChunk Id: 5eac2d86-b354-4452-9ada-12871757ea28
19. Understanding CompiledIR¶
The CompiledIR is an optimized, flattened representation of the IR that's used for efficient rendering and diff computation.
It builds efficient indices into the original prompt tree and is useful analayzing and optimizing te prompt.
# Create a nested prompt to see compilation in action
inner = prompt(t"Inner: {'data':data}")
items = [prompt(t"Item {str(i):item_{i}}") for i in range(3)]
p = dedent(t"""
Header text
{inner:nested}
List: {items:items:sep=, }
""")
# Access the compiled IR
compiled = p.ir().compile()
print(f"CompiledIR type: {type(compiled).__name__}")
CompiledIR type: CompiledIR
Gotchas¶
IDs are not stable across clones¶
Each node in a StructuredPrompt has an ID that's used for de-duping during JSON export and indexing. However, these IDs are not stable across clones or separate constructions of the same prompt.
Don't rely on IDs for identity comparison between prompts. Instead, use StructuralDiff to compare prompts semantically based on their structure and content, not their internal IDs.
Repeated keys raise exceptions¶
By default, having duplicate keys within the same StructuredPrompt raises an exception. Each key must be unique at its level of the hierarchy.
# This will raise an error:
prompt(t"{a:key} and {b:key}") # ❌ Duplicate key 'key'
Why? Repeated keys prevent effective structural diffing and make navigation ambiguous. If you need to represent repeated items, use list interpolations instead.
Prompts are single-use (requires cloning for reuse)¶
Following patterns from ASTs and DOM elements, a StructuredPrompt can only be parented once. You cannot interpolate the same prompt object in multiple places.
p = prompt(t"reusable content")
parent = prompt(t"{p:first} and {p:second}") # ❌ p already has a parent
Solution: Use clone() to create independent copies:
p = prompt(t"reusable content")
parent = prompt(t"{p:first} and {p.clone():second}") # ✅ Works
Alternatively, use a factory function that constructs a fresh prompt each time you need it.
Summary¶
You've learned all the major features of t-prompts:
✅ Basics: t-strings, format specs, dedenting, composition
✅ Content Types: XML tags, headers, images, LaTeX, code, lists, tables
✅ Navigation: Accessing interpolations, mapping protocol, nested structures
✅ Introspection: Source tracking, JSON export, provenance
✅ Diffs: Rendered and structural differences
✅ Internals: IR and CompiledIR representations