Fundamentals

Few-Shot Prompting in Python with Examples

Stop describing the output you want and show it instead. Wire two or three worked examples into your messages list in Python, then measure what changed.

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.

How example pairs sit inside the messages list Three rows of paired boxes: the first two rows are worked examples written by you as a user message and an assistant message, and the bottom row is the real request and the reply that copies the demonstrated shape. user messages assistant messages user Example input 1 assistant Example output 1 user Example input 2 assistant Example output 2 user Your real input assistant Same shape back
You write the top four boxes yourself and send them on every call; only the bottom-right box is generated, and it copies the shape of the two assistant turns above it.

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.

Output shapes from ten inputs, before and after adding examples Two panels side by side, each holding ten bars that stand for ten replies. The zero-shot panel shows bars of many different lengths while the few-shot panel shows ten identical bars. Same ten inputs Zero-shot run many shapes back Few-shot run one shape, ten times Count the shapes fewer is better
Each bar is one reply and its length stands for that reply's fingerprint; the score you care about is how many distinct lengths a column contains, not how good any single answer reads.

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

ExamplesTypical effect
0Content is usually fine; format wanders between calls.
1Format locks on, but the model over-copies that example's topic and length.
2-3Best value: shape is stable and variety stops the model from overfitting to one case.
8 or moreSmall 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.RateLimitError during 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

Deciding whether to add examples or reach for something else A decision tree starting from unstable zero-shot output, branching to shipping as is or adding two to three examples, and then branching again to stopping there or moving to caching and fine-tuning. Zero-shot output keeps changing shape no yes Ship it no examples needed Add 2-3 examples as message pairs shape holds too costly Stop here keep 2-3 examples Prompt too long cache or fine-tune
Examples are the middle rung: reach for them when written rules stop working, and move on when the repeated input tokens start costing more than the consistency is worth.
  • 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.

Frequently asked questions

What is few-shot prompting?

Few-shot prompting means showing a model a small number of finished input-and-output pairs before you ask your real question. Instead of describing the format you want in words, you demonstrate it two or three times. The model copies the pattern it sees, which makes replies far more consistent than a written description alone.

How many examples should I include in a few-shot prompt?

Two or three is the sweet spot for formatting tasks. One example fixes the obvious shape but gets copied too literally. Beyond about eight you pay for a lot of repeated input tokens for a small extra gain. Add examples one at a time and stop as soon as the output shape stops changing.

Should I put my examples in the system prompt or as separate messages?

Use separate messages. Append each example as a user message followed by an assistant message, then add your real input as the final user message. Chat models are trained on that alternating structure, so they recognise it as a demonstrated conversation rather than as text to summarise or comment on.

Does few-shot prompting cost more?

Yes. Your examples are input tokens and they are re-sent on every single call, so three medium examples might triple the input side of each request. Output tokens usually shrink because replies get tighter. Count tokens on both versions before you decide whether the consistency is worth the bill.

Why is my few-shot prompt still returning the wrong format?

The usual cause is examples that disagree with each other, or examples whose formatting differs from what you asked for in words. The model copies what it sees, not what you claim. Print your examples exactly as they will be sent, check every one against your target shape, and fix the odd one out.