Command Palette

Search for a command to run...

Hectal
PHASE 3Beginner ~18 min· topic 7 of 9

Topic 3.7

Prompt Templates as Code

In one line

A prompt template is text with named blanks, filled by a function. A real template engine fills each blank once, complains about missing values, and never lets an inserted value act as a new blank.

Think of it like this

A form letter. "Dear ____, your order ____ has shipped." The shop prints one letter and fills the blanks for each customer. Nobody retypes the letter, and a clerk who sees a blank left empty stops and asks. A prompt template is that letter, and the function that fills it is the clerk.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Template
Text with named blanks such as {{name}} that a function fills with values.
Placeholder
One blank in a template, like {{order}}.
Strict mode
Raising an error for a missing value instead of silently continuing.
Single pass
Reading the template once and never rescanning the text that was inserted.
String soup
Building prompts by gluing strings together all over the code.
Regular expression
A compact pattern for finding text, such as \{\{(\w+)\}\} for a placeholder.

Step by step

01Step 1: string soup

Three features each build their own prompt by gluing strings. One forgets a space, another puts the customer's name in twice. You need to see the whole prompt in one place to review it.

02Step 2: one template, named blanks

"Reply to {{name}} about order {{order}}." A regular expression finds each {{word}}, and a function returns the value for that word. In one pass over the template, every blank is filled.

render.pywhole filepython
import re

def render(template, values):
    return re.sub(r"\{\{(\w+)\}\}", lambda m: values[m.group(1)], template)

print(render("Hi {{name}}", {"name": "Ana"}))

03Step 3: fail loudly on a missing value

If values has no name, the lambda raises a KeyError today. A friendlier version raises ValueError("missing variable: name") so the message says what to fix. The example prints that message.

Step 3: fail loudly on a missing valuediagram
Rendering diagram…

04Step 4: the replace-in-a-loop bug

The naive version loops over the values and calls replace on the growing text. A value containing {{api_key}} becomes a blank for the next loop round, and the stand-in secret appears in the prompt. The single-pass version leaves it as plain text.

05Step 5: put it in a file

Move the template text into prompts/reply.txt and load it once. The function stays the same. Now a reviewer can read the prompt without reading code, and a diff shows only prompt changes.

Try it yourself

  1. 1

    Predict the leak

    Before running, write down what the naive line prints. Which variable is processed first, and which replacement exposes the stand-in secret?

  2. 2

    Reorder the dictionary

    Move api_key to the start of values (in Java and JavaScript too). Run again. The naive version no longer leaks, because the loop reaches api_key before name is filled. The bug depends on dictionary order, which is why it is hard to notice.

  3. 3

    Add a second template

    Add Greet {{name}} politely. and render it with the same values. Which function would you reuse, and where would you put the escaping from Topic 3.4 so both templates are protected?

Code & diagrams

Naive replace vs a single-pass template filler New tab

The secret is a harmless stand-in string. The point is the structure: a value must never act as a new placeholder.

Sign in to run this example in your browser.

Expected output

naive: Reply to Ana KEY-123 (stand-in secret) about order A-17.
safe:  Reply to Ana {{api_key}} about order A-17.
error: missing variable: name

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Silently ignore a missing variable

Change render so a missing key uses an empty string instead of raising (in Python values.get(key, ""), in JavaScript values[key] ?? "", in Java values.getOrDefault(key, "")). Then make the last call print its result: print(render(TEMPLATE, {"order": "A-17"})) (in the other languages print the returned text).

terminal
$ Run
── what you'll see ──
naive: Reply to Ana KEY-123 (stand-in secret) about order A-17.
safe: Reply to Ana {{api_key}} about order A-17.
Reply to about order A-17.

Myth vs fact

Myth

String formatting or f-strings are enough.

Fact

For one prompt in one place, yes. Once prompts live in files, are shared, or contain untrusted values, you need one filler with strict mode and escaping.

Myth

A template engine makes untrusted input safe.

Fact

It only prevents values acting as blanks. You still need escaping and delimiters (Topic 3.4) and the other defences in Phase 10.

Myth

Prompts should be hidden in code for security.

Fact

Prompts in files are easier to review and test. Security comes from not putting secrets in them and from output checks, not from hiding the text.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    Add a size check in the filler. A value that is accidentally 200,000 characters long (a whole log file) turns a cheap call into an expensive one. Reject or trim values above a limit and log it (Phase 6).

  • ▸

    Return both the prompt and a record of the inserted variable names and lengths. When a user reports a strange answer, that record, not the secret contents, tells you what went in.

  • ▸

    Test templates like code: render with sample values and compare with an expected file (a snapshot). A reviewer then sees exactly how a template edit changes the final prompt.

Remember this

  1. 1

    Why this exists. The quick way to build a prompt is string soup: "Reply to " + name + " about " + order + "..." scattered through the code. It works until the third feature, when nobody can find the full prompt, a variable is forgotten, a quote breaks the text, or a value contains something that looks like another blank. A template keeps the whole prompt in one readable place, with the blanks marked.

  2. 2

    Named blanks. Mark blanks like {{name}} and fill them from a dictionary. The reader sees the whole prompt at once, and the code only supplies values. Libraries do this (Jinja in Python, Handlebars or template literals in JavaScript, String.format or text blocks in Java); the example builds the smallest version by hand so you see every rule.

  3. 3

    Fail on a missing value. If a blank has no value, an engine that quietly leaves {{name}} in the prompt, or inserts None/undefined, sends nonsense to a paid model. A strict engine raises an error ("missing variable: name"). The example does exactly that. Fail early, in development, for free.

  4. 4

    Fill in one pass. The example shows a classic bug. The naive filler replaces one variable after another over the whole text. If a user's name is Ana {{api_key}}, the first replacement puts that text into the prompt, and the later api_key replacement then finds it and inserts the key. A value has become a new blank. The safe filler scans the template once and never looks inside inserted values.

  5. 5

    Injection-safe filling. Untrusted values should also be escaped or fenced before insertion (Topic 3.4). Put that in the one place that fills templates, not in each caller, so it cannot be forgotten. Keep trusted constants (your instructions) and untrusted values (user text) in clearly different variables.

  6. 6

    Template files. As prompts grow, move them out of code into files (prompts/ticket-label.txt) loaded at startup. Non-programmers can review them, diffs show exactly what changed, and Topic 3.8 gives each file an id and a version. Keep the filling code small and tested; keep the words in the files.

Explain it without notes

01

What problems does a prompt template solve compared with building strings by hand?

02

Explain the replace-in-a-loop bug and the fix.

03

Why should a template engine raise an error on a missing variable?

Practice

01

Write render(template, values) that replaces {{name}}-style blanks in one pass and raises ValueError("missing variable: <name>") for a blank with no value.

02

Write variables_in(template) that returns the blank names in the order they first appear, without duplicates.

Trade-offs

  • ↔

    A hand-written filler is tiny and transparent; a library (Jinja and similar) adds loops and conditions but also a language to learn and secure.

  • ↔

    Templates in files help reviewers but add a loading step and a place for the file and the code to drift apart; test that every variable the file needs is supplied.

  • ↔

    Strict mode catches bugs early but needs real defaults for genuinely optional parts.

Done when you can

  • Done when you can write a template with named blanks and fill it in one pass.

  • Done when you can explain the replace-in-a-loop bug.

  • Done when your filler raises an error naming a missing variable.

  • Done when you can say where escaping and size limits belong.