Prompts are templates
A prompt with blanks in it is a template: fixed instructions, variable slots, and a rule about what happens when a slot is empty.
The same instructions, different facts
Almost every prompt is the same paragraph with one or two things changed: the user’s question, a piece of context, a tone, a language. Writing that paragraph again for every call is how prompts drift — one copy says “answer briefly”, another forgot to, and now the same application behaves differently depending on which branch ran.
A template fixes the instruction once and leaves named slots for the parts that vary. {topic} and {audience} are not special syntax; they are placeholders that a formatting step replaces before the text is sent.
The drift is easy to miss because each copy looks reasonable on its own. One branch says “answer briefly”, another says “be concise”, a third has no limit at all; the application then appears inconsistent depending on which route the request took. A template makes that impossible: there is one instruction, and a change to it is a single diff that every caller inherits.
Roles are part of the template
Chat models do not take one blob of text. They take a list of messages, each with a role: system sets the behaviour, human is what the user said, ai is what the model said before. A template for a chat model therefore produces a list, not a string.
Keeping the system message separate is not cosmetic. It is the part you control most strongly, it is usually the part you cache, and it is the part that should never contain untrusted user text. A template that concatenates everything into one string throws that separation away.
It also means the template can be cached by role: the system block is identical across calls, while the human block changes. Providers price cached input differently, and keeping the stable text in its own message is what makes that possible. A template that merges the two gives up that option for no gain.
A chat prompt template
from langchain_core.prompts import ChatPromptTemplate
prompt = ChatPromptTemplate.from_messages([
("system", "You are a {role}. Answer in at most two sentences."),
("human", "{question}"),
])
messages = prompt.format_messages(role="database tutor", question="What is an index?")
for message in messages:
print(message.type, "→", message.content)A template with named slots
Call `render` with a missing variable and read the error. Then call it with an empty string and see why that case needs its own guard.
Validate at the edge
Missing and empty are different failures and deserve different messages. A missing key is a programming error: the caller forgot an argument, and the template should raise before a model call is paid for. An empty value is a data error: the caller supplied a slot with nothing in it, and the prompt builder cannot know whether that is legitimate.
A common mistake is handling both with the same guard deep inside the template. The symptom is an error message that says “missing audience” when the audience was supplied as an empty string, which sends the reader looking in the wrong place. Validate where the value enters, with a message that names the actual problem.
Missing values should fail loudly
"{topic}".format() raises KeyError when the caller forgets a value, and that is the behaviour you want: a prompt that silently reads “Explain in one sentence” because topic was never supplied is worse than a crash, because it still costs a model call and returns a plausible answer to the wrong question.
The subtle case is an empty string. Formatting succeeds, the prompt looks fine, and the model answers about nothing. Treat empty as missing at the boundary where the value enters, not deep inside the prompt builder where the context is gone.
Never interpolate raw user text into the system message. Keep instructions and data in separate messages, or a question can rewrite the rules it is supposed to be answered under.
Templates are testable data
Because a template is a string plus a substitution rule, it can be asserted like any other value: given these inputs, the rendered prompt is exactly this. That test catches the change nobody meant to make — a reworded instruction that quietly removed the length limit.
It also makes prompts reviewable. A diff on a template shows the instruction that changed, which is far easier to discuss than a diff on a chain that builds the string inline.
Messages, not one string
The builder produces a message list. Add a fourth role for a retrieved document and place it before the human question, so the instructions stay separate from the data.
Check what you learned
One question at a time. A wrong answer explains itself — take another go.
Part 1 of 5
What problem does a prompt template solve?
