Prompt Templates
Reusable prompts with variables, system and human messages, and few-shot examples.
What you will be able to do
- Turn a hard-coded prompt into a template with variables
- Fill a template, and see exactly what reaches the model
- Separate behaviour (system) from the request (human) with from_messages
- Use few-shot examples to show the model the answer you want
- Avoid the template traps: missing variables, leaking indentation, and literal braces
The idea, in plain English
In Lesson 2.2 the prompt was a fixed string: "Explain Docker in simple English." To explain Kubernetes, Redis, and FastAPI too, you would copy it three times. A prompt template keeps the wording once and leaves gaps for what changes: "Explain {topic} in simple English."
At run time you supply the values - chain.invoke({"topic": "Docker"}) - and LangChain fills the gaps before anything reaches the model. The model never sees {topic}; it sees "Explain Docker in simple English." The template is the reusable structure; the dictionary is the data.
Templates also carry behaviour. A system message says how to answer, a human message carries the request, and few-shot examples show what a good answer looks like. In our runs, two short examples cut the model’s answers from over 180 words to about 10.
Every output here comes from a real run against a local model (llama3; the code says llama3.1, the course model).
Worked example: A reusable "explain like I am 5" prompt.
1 - The template is written once
Fixed wording, with gaps in braces. ChatPromptTemplate reads the gaps as input variables: ["level", "topic"].
Step through one call. Watch where the values go in - and notice the model only ever sees the finished text.
Templates and variables
ChatPromptTemplate.from_template("Explain {topic} in simple English.") finds every name in braces and records it as an input variable - here, just topic. A template can have as many as you need: "Explain {topic} to a {level} developer" has two.
Values come in a dictionary whose keys match the names. Leave one out and the chain stops before the model with a KeyError naming it: "missing variables {’level’}". Extra keys are ignored silently.
To see exactly what the model will get, invoke the template on its own: prompt.invoke({"topic": "Docker"}) returns the finished messages. It costs nothing - no model is called.
Why not an f-string?
f"Explain {topic}" fills the gap the moment the line runs, so it is not reusable - it is just a string. A template stays a template until it is invoked, knows which variables it needs and reports the missing ones, and joins onto a model with |.
It can also be filled in stages. prompt.partial(level="beginner") returns a new template with level already set, leaving only topic - handy when one value is fixed for a whole app and the other changes per request.
System and human messages
from_messages builds a template from roles: a system message for behaviour - "You are a programming teacher. Use simple English." - and a human message for the request - "Explain {topic}." Either can contain variables.
Keeping them apart means the behaviour is written once and reused for every request, and the request stays short. It is Module 1’s system and user roles, with gaps.
Few-shot examples
Instructions describe the answer you want; examples show it. Put two or three question-and-answer pairs in front of the real question, and the model copies their length, tone, and structure.
The effect is large. Without examples, llama3 answered "What is Kubernetes?" in 224 words, "What is an API?" in 183, and "What is PostgreSQL?" in 324. With two one-sentence examples in front, the same questions took 10, 21, and 9 words.
Examples teach a pattern, not facts. They should be correct anyway - the model may lean on them - and they should look like the answers you actually want, because that is what you will get.
What is Kubernetes?No examples: 224 words. Two examples: 10 words.What is an API?No examples: 183 words. Two examples: 21 words.What is PostgreSQL?No examples: 324 words. Two examples: 9 words.FewShotChatMessagePromptTemplate
Writing examples into one long string works. LangChain’s FewShotChatMessagePromptTemplate does it more cleanly: give it a list of example dictionaries and a small template for one example, and it turns them into real human and AI turns - exactly as if the conversation had happened.
Our run produced system, human ("What is Docker?"), ai, human ("What is Redis?"), ai, and finally the real question - and the answer came back in 15 words. Keeping examples as data also means you can store them, swap them, or choose different ones per request.
Three template traps
Literal braces. Anything in braces is a variable - including the braces in a JSON example. ’Reply as JSON like {"name": "..."}’ creates a variable called "name", builds without complaint, and fails at invoke with "missing variables". Double the braces - {{"name": "..."}} - to mean literal braces. This bites every JSON prompt from Lesson 1.3 you move into a template.
Indentation. A system message written as an indented triple-quoted string is sent exactly as written: "\n You are a programming teacher.\n ..." - leading newline, eight spaces per line. It rarely changes the answer, but it is noise in every call. Start the text at the left margin, or strip it.
Missing variables. Every name in braces needs a key in the dictionary. Check prompt.input_variables when in doubt.
Watch out: A template with literal braces builds fine and only fails when it runs - with an error about a variable you did not mean to create. Double any brace that is not a variable.
Step-by-step code
from langchain_core.prompts import ChatPromptTemplate
from langchain_ollama import ChatOllama
llm = ChatOllama(model="llama3.1", temperature=0)
prompt = ChatPromptTemplate.from_template("Explain {topic} in simple English.")
chain = prompt | llm
for topic in ["Docker", "Redis", "FastAPI"]:
print(chain.invoke({"topic": topic}).content)prompt = ChatPromptTemplate.from_template("Explain {topic} for a {level} developer.")
print(prompt.input_variables)
# ['level', 'topic']
print(prompt.invoke({"topic": "Redis", "level": "beginner"}).to_messages())
# [HumanMessage(content='Explain Redis for a beginner developer.')]
prompt.invoke({"topic": "Redis"})
# KeyError: Input to ChatPromptTemplate is missing variables {'level'}.
# Expected: ['level', 'topic'] Received: ['topic']
beginner = prompt.partial(level="beginner")
print(beginner.input_variables) # ['topic']eli5 = ChatPromptTemplate.from_template(
"Explain {topic} like I am 5 years old, in 3 short sentences."
) | llm
print(eli5.invoke({"topic": "a database"}).content)Here's an explanation of a database that a 5-year-old can understand:
A database is like a super cool, magic bookshelf that can hold lots and lots of
information, like pictures and words. You can put things on the bookshelf, like a
picture of your favorite toy, and then you can find it again later by looking at the
bookshelf. It's like a special place where you can store all your favorite things and
find them again whenever you want!prompt = ChatPromptTemplate.from_messages([
("system", "You are a programming teacher. Explain concepts in simple English. "
"Keep the answer concise. Give one real-world example."),
("human", "Explain {topic} for a {level} developer."),
])
chain = prompt | llm
print(chain.invoke({"topic": "Redis", "level": "beginner"}).content)few_shot = ChatPromptTemplate.from_template("""You explain programming concepts in one short sentence.
Question: What is Docker?
Answer: Docker packages applications and their dependencies into containers.
Question: What is Redis?
Answer: Redis is an in-memory data store commonly used for caching.
Question: {question}
Answer:""")
chain = few_shot | llm
print(chain.invoke({"question": "What is Kubernetes?"}).content) no examples two examples
"What is Kubernetes?" 224 words 10 words
"What is an API?" 183 words 21 words
"What is PostgreSQL?" 324 words 9 words
With examples:
Kubernetes automates the deployment, scaling, and management of containerized applications.
PostgreSQL is a powerful open-source relational database management system.from langchain_core.prompts import ChatPromptTemplate, FewShotChatMessagePromptTemplate
examples = [
{"question": "What is Docker?", "answer": "Docker packages applications and their dependencies into containers."},
{"question": "What is Redis?", "answer": "Redis is an in-memory data store commonly used for caching."},
]
shots = FewShotChatMessagePromptTemplate(
examples=examples,
example_prompt=ChatPromptTemplate.from_messages([("human", "{question}"), ("ai", "{answer}")]),
)
prompt = ChatPromptTemplate.from_messages([
("system", "You explain programming concepts in one short sentence."),
shots,
("human", "{question}"),
])
print((prompt | llm).invoke({"question": "What is Kubernetes?"}).content)system You explain programming concepts in one short sentence.
human What is Docker?
ai Docker packages applications and their dependencies into containers.
human What is Redis?
ai Redis is an in-memory data store commonly used for caching.
human What is Kubernetes?
Kubernetes is a container orchestration system that automates the deployment,
scaling, and management of containers. (15 words)# Literal braces become variables
bad = ChatPromptTemplate.from_template('Reply as JSON like {"name": "..."}. Person: {person}')
print(bad.input_variables) # ['"name"', 'person'] - not what you meant
bad.invoke({"person": "Priya"}) # KeyError: missing variables {'"name"'}
good = ChatPromptTemplate.from_template('Reply as JSON like {{"name": "..."}}. Person: {person}')
print(good.invoke({"person": "Priya"}).to_messages()[0].content)
# Reply as JSON like {"name": "..."}. Person: Priya
# Indented triple-quoted text is sent as written
indented = ChatPromptTemplate.from_messages([("system", """
You are a programming teacher.
"""), ("human", "{q}")])
print(repr(indented.invoke({"q": "x"}).to_messages()[0].content))
# '\n You are a programming teacher.\n 'Tip: prompt.invoke(values) shows the exact messages the model will receive, without calling the model. Check it whenever an answer surprises you.
Prompt templates at a glance
from_templateOne human message with variables.
ChatPromptTemplate.from_template("Explain {topic}.")from_messagesSeveral roles, each with variables.
[("system", "..."), ("human", "Explain {topic}.")]input_variablesThe names the template needs.
prompt.input_variables # ['topic']
invokeFill the template - no model call.
prompt.invoke({"topic": "Docker"})partialPre-fill some variables.
prompt.partial(level="beginner")
Few-shotExamples as real conversation turns.
FewShotChatMessagePromptTemplate(examples=..., example_prompt=...)
Literal bracesDouble them.
{{"name": "..."}}Try it yourself
The code does not change. Swap the content string and the program does something else entirely.
“Run the ELI5 chain for "the internet", "a CPU", and "an API".”
“Ask three questions with and without the examples and count the words.”
“Print prompt.invoke(values).to_messages() before every model call for one session.”
“Put a JSON example in a template without doubling the braces, and read the error.”
What usually goes wrong
Every name in braces needs a value; the chain stops with a KeyError before the model.
✗ chain.invoke({"topic": "Docker"}) # template also needs {level}✓ chain.invoke({"topic": "Docker", "level": "beginner"})Braces in a JSON or code example become variables. Double them.
✗ 'Reply as JSON like {"name": "..."}'✓ 'Reply as JSON like {{"name": "..."}}'The indentation and the leading newline are sent to the model on every call.
✗ ("system", """
You are a teacher.
""")✓ ("system", "You are a teacher.")An f-string is filled immediately, so it cannot be reused, checked, or chained.
✗ f"Explain {topic} simply."✓ ChatPromptTemplate.from_template("Explain {topic} simply.")The model copies the examples’ length and style. Long, chatty examples get long, chatty answers.
Key points
- A prompt template is reusable wording with variables in braces.
- invoke({...}) fills the variables; the model only ever sees the finished text.
- from_messages separates behaviour (system) from the request (human).
- Few-shot examples show the pattern - two examples cut answers from 180+ words to about 10.
- FewShotChatMessagePromptTemplate turns example data into real conversation turns.
- Double literal braces, and do not indent triple-quoted prompts.
- prompt.invoke() shows exactly what the model will receive, without calling it.
Quick check before you move on
Quiz
- 1.
A template contains ’Reply as JSON like {"name": "..."}’ and a {person} variable. What happens?
- 2.
You pass a key the template does not use. What happens?
- 3.
What does prompt.partial(level="beginner") return?
- 4.
Why did two short examples shorten the answers so much?
Interview questions
Why use a prompt template instead of building strings by hand?
It separates the reusable prompt structure from run-time data, validates that every variable is supplied, can be partially filled, and composes with models and parsers. That makes prompts easier to reuse, test, and maintain.
What is few-shot prompting, and when would you use it?
Including a few input-output examples in the prompt so the model follows their format, length, and style. Use it when instructions alone do not produce consistent output - especially for format and tone.
What is a common bug when moving prompts into templates?
Literal braces - in JSON or code examples - are parsed as variables. The template builds but fails at run time. Escape them by doubling.
Comments
Sign in to leave a comment. Your name and photo come from Google; nothing else is shared.
Loading comments...
AI
System Design
Backend
- GraphQL8 modules · 69 lessons planned
- Core Python13 modules · 75 lessons planned
- FastAPI5 sections · 20 lessons
- Node.js14 modules · 206 lessons planned
- Node.js Performance7 chapters · 36 topics
- Event Loop Lifecycle6 phases · 3 scenarios
- Docker & Containerization11 modules · 144 lessons planned
- AWS for Developers14 modules · 219 lessons planned
- CI/CD & DevOps Automation10 modules · 134 lessons planned