By the end of this guide you will have a Python script that sends two or three worked examples to a model before your real question, and a second script that proves the examples helped by running ten inputs through both versions and comparing the results. Budget about forty minutes, including the time the test loop spends waiting on the API.
This is one of the guides in Prompt Engineering Basics with Python, and it picks up where written instructions run out of road. A system prompt tells the model what you want in words; few-shot prompting shows it. When you have written three increasingly desperate paragraphs of formatting rules and the output still wobbles, showing is the fix.
Prerequisites
You need Python 3.10 or newer, a virtual environment, and an OpenAI API key. If any of those is missing, start with Create a Python Virtual Environment for AI and come back.
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install "openai>=1.40" "python-dotenv>=1.0" "tiktoken>=0.7"
Put your key in a file called .env next to your script:
OPENAI_API_KEY=sk-your-key-here
Then add that file to .gitignore before you write another line, because a key committed to a public repository is found and spent by strangers within minutes:
echo ".env" >> .gitignore
Everything below uses one running task: turning a messy customer support message into a small, fixed record with three fields. It is a good test case because there is exactly one right shape and a hundred plausible wrong ones.
Step 1 — Write the zero-shot version and watch it drift
A zero-shot prompt is one with no examples: you describe the job and hope. Write it first, because you cannot tell whether examples helped if you never measured the version without them.
import os
from dotenv import load_dotenv
from openai import OpenAI
load_dotenv() # .env is in .gitignore
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
TICKETS = [
"The invoice PDF from March never arrived and my card was charged twice.",
"Hi! Quick one - can I move my plan to annual billing before renewal?",
"App crashes every time I tap export. Nothing in the logs. Please help.",
]
for ticket in TICKETS:
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "user",
"content": f"Triage this support message into category, "
f"urgency and a one-line summary:\n\n{ticket}"},
],
temperature=0,
)
print(response.choices[0].message.content)
print("-" * 40)
Run it. Even with temperature set to 0 — the setting that makes replies as repeatable as the API allows, explained in What Is Temperature in an LLM API? — you will get three different presentations. One reply arrives as a bulleted list, one as a short paragraph, one as a table. The categories will be invented on the spot: Billing, billing issue, Payments. Every one of those answers is reasonable. None of them is parseable by the next line of your program.
That is the real problem with zero-shot output. It is not wrong, it is unpredictable, and unpredictable output cannot be fed into a spreadsheet, a database, or another prompt.
Step 2 — Add your examples as alternating messages
Here is the part most people get wrong. The instinct is to paste the examples into one giant string and send it as a single user message. That works badly: the model sees a wall of text about examples and often starts discussing them, or repeats them back, or treats the whole block as the thing to summarise.
The structure that works mirrors how chat models were trained. You build a fake conversation that already happened: a user message containing an example input, an assistant message containing the exact output you wanted for it, then the next pair, and finally your real input as the last user message. The model reads a conversation where the assistant has already answered twice in a particular shape, and continues the pattern.
In code, that list is built with a loop, so adding or removing an example is a one-line edit:
import os
from dotenv import load_dotenv
from openai import OpenAI
load_dotenv()
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
SYSTEM = (
"You triage customer support messages. "
"Reply with a single JSON object and nothing else."
)
EXAMPLES = [
("My card was declined at checkout twice this morning.",
'{"category": "billing", "urgency": "high", '
'"summary": "Card declined twice at checkout"}'),
("Do you have a dark mode on the mobile app?",
'{"category": "feature_request", "urgency": "low", '
'"summary": "Asks for dark mode on mobile"}'),
("Export button spins forever on a 40MB file.",
'{"category": "bug", "urgency": "medium", '
'"summary": "Export hangs on large files"}'),
]
def build_messages(ticket: str) -> list[dict]:
"""System rules, then each example as a user/assistant pair, then the real input."""
messages = [{"role": "system", "content": SYSTEM}]
for example_in, example_out in EXAMPLES:
messages.append({"role": "user", "content": example_in})
messages.append({"role": "assistant", "content": example_out})
messages.append({"role": "user", "content": ticket})
return messages
def triage(ticket: str) -> str:
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=build_messages(ticket),
temperature=0,
)
return response.choices[0].message.content
if __name__ == "__main__":
print(triage("The invoice PDF from March never arrived "
"and my card was charged twice."))
Run this and you get one line of JSON with exactly the three keys you demonstrated. Notice how short the system prompt became. The examples carry the formatting load, so the written rules only have to state the job. That division of labour — words for the task, examples for the shape — is the whole technique. For the words half, see Write System Prompts that Control Output Format.
Step 3 — Pick examples that are short, varied, and already the right shape
The model copies your examples with unnerving loyalty, including their flaws. Three rules keep that loyalty working for you.
Short. Every example is re-sent on every call forever. Trim each one to the minimum that still demonstrates the pattern. If your example input runs to a paragraph, cut it to a sentence and check whether the output still holds its shape.
Varied. Pick examples that sit in different corners of your data. In the code above, one is a billing complaint, one is a feature request, one is a bug — and their urgencies are high, low and medium. Three examples of the same category teach the model that everything is that category. This is the most common failure and the easiest to avoid: read your three examples and ask whether a stranger could guess the rule from them alone.
Already correct. Your example outputs are the specification. If you say "keys in snake_case" in the system prompt but one example uses featureRequest, the model follows the example. Write the examples first, then write a system prompt that describes them, not the other way round.
One more habit worth building: keep the examples in a plain Python list, not glued into the prompt string. A list is easy to print, easy to loop over in a test, and easy to swap per task. If you later hold different example sets for different document types, the same build_messages function serves all of them — a pattern that scales nicely into work like Classify Incoming Documents with Python and AI.
Step 4 — Measure the improvement across ten inputs
"It looks better" is not a result. Run the same ten inputs through both versions and compare the shape of what comes back, not the wording. Shape is the thing you actually depend on downstream.
The script below defines a shape_of function that reduces any reply to a short fingerprint — either not-json or a sorted list of its top-level keys — then counts how many distinct fingerprints each version produced. A perfect run scores one.
import json
import os
from collections import Counter
from dotenv import load_dotenv
from openai import OpenAI
load_dotenv()
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
from few_shot import SYSTEM, EXAMPLES, build_messages # from Step 2
TEST_TICKETS = [
"Charged twice for March, still no invoice.",
"Can I switch to annual billing before renewal?",
"App crashes when I tap export on a big file.",
"Please delete my account and all my data.",
"Is there a student discount?",
"Login emails land in spam every single time.",
"Your pricing page 404s on mobile Safari.",
"I need an extra seat added to the team plan.",
"Dashboard totals disagree with the CSV export.",
"How do I change the billing address on receipts?",
]
def ask(messages: list[dict]) -> str:
response = client.chat.completions.create(
model="gpt-4o-mini", messages=messages, temperature=0,
)
return response.choices[0].message.content.strip()
def shape_of(reply: str) -> str:
"""Reduce a reply to a fingerprint we can compare across runs."""
try:
data = json.loads(reply)
except json.JSONDecodeError:
return "not-json"
if not isinstance(data, dict):
return "not-an-object"
return ",".join(sorted(data.keys()))
zero_shapes, few_shapes = Counter(), Counter()
for ticket in TEST_TICKETS:
zero = ask([{"role": "system", "content": SYSTEM},
{"role": "user", "content": ticket}])
few = ask(build_messages(ticket))
zero_shapes[shape_of(zero)] += 1
few_shapes[shape_of(few)] += 1
print("zero-shot shapes:", len(zero_shapes), dict(zero_shapes))
print("few-shot shapes:", len(few_shapes), dict(few_shapes))
Save the Step 2 script as few_shot.py so the import works, then run this one. The zero-shot column typically returns a handful of different fingerprints — some JSON with extra keys, some prose that fails to parse at all — while the few-shot column collapses to a single fingerprint repeated ten times. That collapse is the entire value of the technique, and now you can point at a number instead of an impression.
If your zero-shot column is full of not-json, that is the same failure described in Fix JSONDecodeError with AI API Responses in Python, and examples are one of the cheapest cures for it.
Step 5 — Count what the examples cost you
Examples are not free. They are input tokens, and they are re-sent on every single call for the life of the script. Three medium examples can easily double or triple the input side of each request, and at high volume that is a real line on your bill. Measure it directly with the token counts the API already returns:
import os
from dotenv import load_dotenv
from openai import OpenAI
from few_shot import SYSTEM, build_messages
load_dotenv()
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
TICKET = "Charged twice for March, still no invoice."
variants = {
"zero-shot": [{"role": "system", "content": SYSTEM},
{"role": "user", "content": TICKET}],
"few-shot": build_messages(TICKET),
}
for name, messages in variants.items():
response = client.chat.completions.create(
model="gpt-4o-mini", messages=messages, temperature=0,
)
usage = response.usage
print(f"{name:>10}: in={usage.prompt_tokens} out={usage.completion_tokens}")
Two things usually show up. Input tokens rise sharply, and output tokens fall, because a model that knows the target shape stops padding its answer with explanation. Whether the trade is worth it depends on your volume: at a few hundred calls a month it is noise, and at a few hundred thousand it deserves an afternoon of tuning. To put a currency figure on the difference, work through Estimate OpenAI API Costs with Python, and to check a prompt's size before you ever send it, see Count Tokens in Python Before You Send.
Three levers reduce the cost without giving up the consistency. Trim each example to its shortest useful form. Drop from three examples to two once the shape is stable. And if the same inputs recur, stop paying for them twice by following Cache AI Responses in Python to Cut Costs.
Quick reference: how many examples to use
| Examples | Typical effect |
|---|---|
| 0 | Content is usually fine; format wanders between calls. |
| 1 | Format locks on, but the model over-copies that example's topic and length. |
| 2-3 | Best value: shape is stable and variety stops the model from overfitting to one case. |
| 8 or more | Small extra accuracy on genuinely subtle rules, at several times the input cost. |
Treat these as starting points and confirm with the Step 4 measurement on your own data. Add one example at a time and stop the moment the shape count stops falling.
Troubleshooting
json.decoder.JSONDecodeError: Expecting value: line 1 column 1— the model wrapped its reply in a fenced code block, so the string starts with backticks. Your examples almost certainly contain the fences too; strip them from the example outputs and the model will stop adding them.- The reply repeats one of your examples verbatim — your real input resembles an example too closely, or you only supplied one example. Add a second and third from clearly different categories so there is a pattern to generalise rather than a single answer to echo.
openai.BadRequestError: ... maximum context length— your examples plus the input exceed what the model accepts in one request. Shorten the examples first, then the input; Fix the Context-Length-Exceeded Error in Python covers the longer-term fixes.openai.RateLimitErrorduring the ten-input test loop — you are sending requests faster than your account tier allows. Add a short pause between calls, or apply the retry pattern in Fix the 429 Rate-Limit Error in Python.
When to use this vs. alternatives
- Use a plain system prompt instead when the rule is easy to state in one sentence and the output is free-form prose. "Reply in British English, under 80 words" needs no demonstration, and examples would only inflate the request.
- Use few-shot examples when the output has a fixed shape, a fixed vocabulary, or a house style you can show more easily than describe — classification labels, structured records, a particular headline voice. This is also the fastest way to encode a brand tone, which is why the recipes in Prompt Engineering Templates for Marketers lean on it heavily.
- Split the work into several calls instead when one prompt is trying to do three jobs at once and no number of examples fixes it. Two focused prompts usually beat one crowded one; Chain Prompts Together in Python shows how to pass output from one step into the next.
- Graduate to fine-tuning when you have hundreds of verified pairs, the task is stable, and you are calling it constantly. Fine-tuning bakes the pattern into a custom model so you stop paying for examples on every request — but it is a bigger commitment, and few-shot prompting is how you gather the training pairs in the first place.
The honest summary is that few-shot prompting buys you consistency with input tokens. Start zero-shot, add two or three short and varied examples the moment the shape wanders, measure the shape count over ten real inputs, and keep the smallest example set that holds. That loop takes an afternoon and it will do more for your output quality than another paragraph of instructions ever would.
Back to Prompt Engineering Basics with Python.
Related guides
- Write System Prompts that Control Output Format — the words half of the job, which pairs directly with the examples half.
- Chain Prompts Together in Python — what to do when one prompt is carrying too many jobs.
- Count Tokens in Python Before You Send — measure what your example set adds to every request.
- What Is Temperature in an LLM API? — the other dial that affects how repeatable your replies are.
- Fix JSONDecodeError with AI API Responses in Python — handle the replies that still refuse to parse.