Stage 01 · FoundationsModule 3 of 26~5h

Prompt Engineering Basics

Master the prompting patterns that work every time.

← All modules in this stage

A "prompt engineer" is just someone who notices when their prompt is the problem and fixes it deliberately. This module gives you a tiny set of patterns that work on every model and a way to debug when they don't.

By the end of this module you'll have

Time: about 1 hour for the basics, ~5 hours with all three notebooks.

Prerequisites: Module 1 and ideally Module 2.


The five-part template

A prompt that works in production almost always has these five pieces — in roughly this order:

1. ROLE        — who Claude is for this task
2. GOAL        — what success looks like, in one sentence
3. CONSTRAINTS — what to do, what not to do
4. EXAMPLES    — one or two demonstrations of good output
5. FORMAT      — exactly how the answer should look

Most "the model gave me garbage" complaints turn out to be one of these missing.


Bad prompt vs good prompt (run both)

Save as prompt_diff.py:

from anthropic import Anthropic
from dotenv import load_dotenv

load_dotenv()
client = Anthropic()

bad_prompt = "Tell me about this product review."

good_prompt = """\
You are a customer-research analyst.                       # ROLE
Goal: classify a product review and pull out the key signal. # GOAL

Rules:                                                       # CONSTRAINTS
- Sentiment must be one of: positive, neutral, negative.
- Be terse. No marketing language.
- If a field is unclear, write "unknown".

Example:                                                     # EXAMPLE
Input:  "Battery dies in 2 hours, otherwise great screen."
Output: {"sentiment":"negative","topic":"battery","quote":"Battery dies in 2 hours"}

Now do the same for this review:
"Setup was painless and audio is crisp, but the app crashes daily on Android."

Respond with one JSON object on a single line, nothing else.   # FORMAT
"""

for label, prompt in [("BAD", bad_prompt), ("GOOD", good_prompt)]:
    response = client.messages.create(
        model="claude-haiku-4-5-20251001",
        max_tokens=300,
        messages=[{"role": "user", "content": prompt}],
    )
    print(f"\n=== {label} ===\n{response.content[0].text}")

The bad version's reply will be a wandering paragraph. The good version's reply will be a single JSON object you can json.loads() without a second thought. Same model, same temperature, completely different downstream usability.


Where to put each piece

client.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=400,
    system="You are a customer-research analyst. Always output JSON. Be terse.",
    messages=[
        {"role": "user", "content": "Battery dies in 2 hours, otherwise great screen."},
        {"role": "assistant", "content": '{"sentiment":"negative","topic":"battery"}'},
        {"role": "user", "content": "Setup was painless and audio is crisp, but the app crashes daily on Android."},
    ],
)

This is few-shot prompting — Claude infers the format from your one demo and applies it to the new input.


A debugging loop for when output is wrong

When a prompt misbehaves, walk this in order — do not just throw more words at it:

  1. Read what you actually sent. Print the full prompt. Often something is missing or duplicated.
  2. Add the missing piece. Which of role / goal / constraints / examples / format is unspecified?
  3. Show, don't just tell. One concrete example beats three abstract instructions.
  4. Pin the format. Say "Reply with one JSON object on a single line, nothing else" — and watch how often that alone fixes things.
  5. Then, and only then, consider going to a bigger model. Most failures are prompt failures.

Try changing one thing


Going deeper: open the notebooks


Module checklist


Next module

Module 4 · Claude API Basics — how to wrap these prompts in code that's robust enough to ship.