Build a System Prompt Workshop
Engineer Claude's behavior through iterative system prompt refinement.
Introduction
30 Second Summary
Every time you use an AI chatbot and get a generic, unfocused answer, the problem is almost never the AI itself. The difference between a useful AI assistant and a mediocre one comes down to a single hidden instruction called the system prompt.
In this project, you will build a System Prompt Workshop that transforms Claude from a generic chatbot into a focused code review assistant. You will layer prompt engineering techniques one at a time and see exactly how each one changes the AI's behavior.
What You'll Build
A Python script that calls the Claude API with four progressively refined system prompts, showing how each technique dramatically changes Claude's output.
By the end of this project, you'll have:
- A working Python script that calls the Claude API with custom system prompts and prints the results.
- A collection of 4 system prompt versions demonstrating role prompting, behavioral constraints, output formatting, and few-shot examples.
- A reusable system prompt template saved to a file that you can adapt for any future AI project.
- Secret Mission: Build an automated prompt scoring harness that evaluates each system prompt version against a quality rubric.
Are there any prerequisites?
You need basic Python knowledge (variables, functions, and strings). No prior AI or API experience is needed.
Set Up Your Environment
Before you can start experimenting with system prompts, you need a working Python environment with access to the Claude API.
In this step, you will install Python, set up the Anthropic SDK, and configure your API key so that your script can talk to Claude.
In this step, get ready to:
- Install Python and create your project folder.
- Install the Anthropic Python SDK.
- Get your API key from the Anthropic Console and set it as an environment variable.
Install Python and create your project folder
This project requires Python 3.9 or higher. Let's check whether you already have it installed.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal (macOS) or Command Prompt (Windows) and press Enter to open it.
- Check your Python version by running this command:
macOS
python3 --version
Windows
python --version
✔️ I see version 3.9 or higher
Your Python is ready to go. Skip ahead to the next action below where you create your project folder.
ⓧ I see an older version
You need Python 3.9 or higher for the Anthropic SDK to work.
- Visit the official Python downloads page: https://www.python.org/downloads/
- Download and install the latest Python 3.x release.
- On Windows, check the box that says Add Python to PATH during installation.
- On macOS, the installer handles the PATH automatically. However, your system may still resolve python3 to the old version. If so, close and reopen your terminal, then re-run python3 --version to confirm.
After installing, close and reopen your terminal, then re-run the version check command above.
ⓧ Command not found
Python is not installed yet. Let's fix that.
- Visit the official Python downloads page: https://www.python.org/downloads/
- Download and install the latest Python 3.x release.
- On Windows, check the box that says Add Python to PATH during installation.
- After the installer finishes, close and reopen your terminal.
- Re-run the version check command above. You should now see a version number like 3.12.x.
Now let's create your project folder on your Desktop so it is easy to find.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir system-prompt-workshop
cd system-prompt-workshop
- Confirm the folder was created by running:
pwd
You should see a path ending in system-prompt-workshop.
Not seeing the right path?
Make sure you ran both commands (mkdir and cd) separately. On Windows, use cd %USERPROFILE%\Desktop instead of cd ~/Desktop.
Help me set up my project folder on my Desktop.
Install the Anthropic Python SDK
The Anthropic Python SDK is the official library for calling the Claude API from Python. It handles authentication, request formatting, and response parsing for you.
- Install the SDK by running this command:
pip install anthropic
What does this command do?
pip is Python's package manager. This command downloads and installs the anthropic library so you can import it in your script later.
You should see output ending with a line like Successfully installed anthropic-....
Seeing a permission error or 'pip not found'?
On macOS, try pip3 install anthropic instead. On Windows, make sure Python was added to PATH during installation. If you see a permissions error, try adding --user at the end of the command.
Help me fix my pip installation issue.
Get your API key and configure your environment
To call the Claude API, you need an API key. This key authenticates your script so Anthropic knows who is making the request.
- Open your browser and go to the Anthropic Console API keys page: console.anthropic.com/account/keys
- Create an account if you don't have one yet. New accounts receive free credits to test the API.
- Click Create Key.
- Give your key a name (e.g. prompt-workshop) and click Create Key.
- Copy the key immediately. You will not be able to see it again after closing this dialog.
Keep your API key safe
Never share your API key or commit it to version control. You will store it as an environment variable so it stays out of your code.
Now set the key as an environment variable in your terminal. This lets the Anthropic SDK find it automatically.
- Set the environment variable by running this command (replace your-api-key-here with the key you just copied):
macOS
export ANTHROPIC_API_KEY=your-api-key-here
Windows
set ANTHROPIC_API_KEY=your-api-key-here
Why an environment variable?
The Anthropic SDK automatically reads ANTHROPIC_API_KEY from your environment. This means you never have to paste your key directly into your Python code, which keeps it secure.
This variable lasts only for the current terminal session. If you close and reopen your terminal, you will need to set it again.
Next, open VS Code so you can start writing your script.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type VS Code and press Enter to open it.
- In VS Code, click File in the top menu bar.
- Click Open Folder.
- Navigate to your Desktop, select the system-prompt-workshop folder, and click Open.
Finally, create the file where you will write all your code.
- In VS Code's Explorer sidebar on the left, right-click in the empty space.
- Select New File.
- Name the file prompt_workshop.py and press Enter.
You should see prompt_workshop.py appear in the sidebar and an empty editor tab open for it.
Don't see the Explorer sidebar?
Press Cmd+Shift+E (macOS) or Ctrl+Shift+E (Windows) to toggle the Explorer panel. If you still don't see your folder, make sure you opened the system-prompt-workshop folder (not a parent folder).
Help me get my VS Code Explorer working.
✔️ Awesome, I've got everything!
Great. Make sure you have saved prompt_workshop.py (even though it is empty for now).
ⓧ I'd like to double check the full code
Your prompt_workshop.py file should be completely empty at this point. No code needed yet. Just confirm the file exists in your system-prompt-workshop folder.
Your environment is ready. Next up, you will make your first API call to Claude and see what happens when you send a code review request with no system prompt at all.
Make Your First API Call
Your environment is ready. Now it's time to see what happens when you ask Claude to review code without giving it any guidance.
In this step, you'll send a code review request to Claude with no system prompt at all. This establishes your "baseline" so you can clearly see the difference each prompting technique makes in the steps ahead.
In this step, get ready to:
- Write a Python script that calls the Claude API.
- Run your first API call with no system prompt.
- Establish a baseline response to compare against refined versions.
Write the baseline code review script
You'll build prompt_workshop.py in two parts. First, you'll set up the test data and a reusable API helper. Then, you'll add the functions that run and display the baseline response.
- In VS Code, open prompt_workshop.py and add the following setup code:
import anthropic
import os
client = anthropic.Anthropic()
# The same user message we'll test against all system prompt versions
USER_MESSAGE = """
Please review this Python code:
def get_user_data(user_id):
query = f"SELECT * FROM users WHERE id = {user_id}"
result = db.execute(query)
password = result['password']
return {"id": result['id'], "name": result['name'], "email": result['email'], "password": password}
"""
MODEL = "claude-haiku-4-5"
def call_claude(system_prompt, user_message):
"""Send a message to Claude with the given system prompt."""
message = client.messages.create(
model=MODEL,
max_tokens=1024,
system=system_prompt if system_prompt else "",
messages=[
{"role": "user", "content": user_message}
],
)
return message.content[0].text
What does this code do?
- client = anthropic.Anthropic() creates an API client that automatically reads your ANTHROPIC_API_KEY environment variable.
- USER_MESSAGE holds a Python function with intentional security vulnerabilities (SQL injection, password exposure). This is the code you'll ask Claude to review.
- MODEL is set to claude-haiku-4-5, a fast and affordable model perfect for a learning project.
- call_claude() is a reusable helper that sends any system prompt and user message to the API and returns the text response.
You won't see a change yet. The next chunk adds the functions that actually run the script.
- Below the call_claude function, add the baseline runner, main entry point, and script trigger:
def run_baseline():
"""Version 0: No system prompt at all."""
print("=" * 60)
print("VERSION 0: No System Prompt (Baseline)")
print("=" * 60)
response = call_claude("", USER_MESSAGE)
print(response)
print()
return response
def main():
print("\nSYSTEM PROMPT WORKSHOP")
print("Testing the same code review request with 4 different system prompts\n")
results = {}
results["baseline"] = run_baseline()
if __name__ == "__main__":
main()
What does this code do?
- run_baseline() calls Claude with an empty string as the system prompt. This means Claude has zero guidance about how to behave.
- main() prints a header and calls run_baseline(). It stores the result in a results dictionary so you can compare versions later.
- if __name__ == "__main__": ensures main() only runs when you execute the script directly.
- Save prompt_workshop.py.
Run the script and observe the response
Your script is ready to call the Claude API for the first time. With no system prompt, Claude will respond however it sees fit.
- Run the script by running this command:
python prompt_workshop.py
You should see a "SYSTEM PROMPT WORKSHOP" header followed by Claude's code review response printed in the terminal.
What should I see?
Claude's baseline response will be a general code review. It might mention security issues, or it might focus on style. The format will be unpredictable. It might be long or short, use bullet points or paragraphs, or cover topics you didn't ask about.
This inconsistency is exactly the problem system prompts solve.
Seeing an authentication error?
- If you see an error mentioning that an API key is required, your ANTHROPIC_API_KEY environment variable is not set. Go back to Step 1 and re-export it in the same terminal session you're using now.
- If you see a ModuleNotFoundError for anthropic, run pip install anthropic again in the same terminal.
- Make sure you're running the command from inside your project folder where prompt_workshop.py lives.
Help me debug my Claude API authentication error.
✔️ Awesome, I've got everything!
Great. Double check you've saved your file before moving on.
ⓧ I'd like to double check the full code
import anthropic
import os
client = anthropic.Anthropic()
# The same user message we'll test against all system prompt versions
USER_MESSAGE = """
Please review this Python code:
def get_user_data(user_id):
query = f"SELECT * FROM users WHERE id = {user_id}"
result = db.execute(query)
password = result['password']
return {"id": result['id'], "name": result['name'], "email": result['email'], "password": password}
"""
MODEL = "claude-haiku-4-5"
def call_claude(system_prompt, user_message):
"""Send a message to Claude with the given system prompt."""
message = client.messages.create(
model=MODEL,
max_tokens=1024,
system=system_prompt if system_prompt else "",
messages=[
{"role": "user", "content": user_message}
],
)
return message.content[0].text
def run_baseline():
"""Version 0: No system prompt at all."""
print("=" * 60)
print("VERSION 0: No System Prompt (Baseline)")
print("=" * 60)
response = call_claude("", USER_MESSAGE)
print(response)
print()
return response
def main():
print("\nSYSTEM PROMPT WORKSHOP")
print("Testing the same code review request with 4 different system prompts\n")
results = {}
results["baseline"] = run_baseline()
if __name__ == "__main__":
main()
Note your baseline for comparison
The response you just received is your baseline. It represents what Claude produces when it has no role, no format rules, and no examples to follow.
In the next steps, you'll add system prompt techniques one by one. Each time, you'll run the same code review request and see how the output changes.
Why keep the same user message?
By testing with the same user message every time, you isolate the system prompt as the only variable. Any difference in Claude's output is directly caused by the system prompt changes you make.
You've proven that Claude can review code, but without a system prompt, the output is unpredictable. Next up, you'll add a role and context to focus Claude's responses.
Add Role and Context
In the last step, you ran your code review request through Claude with no system prompt. The response was probably helpful but unfocused, covering many topics without clear priority.
A system prompt tells Claude who it is and how to behave before it ever sees the user's message. In this step, you'll define Claude as a senior security-focused code reviewer and see how that single addition transforms the response.
In this step, get ready to:
- Define a system prompt that gives Claude a role and focus area.
- Run the updated script and compare version 1 to your baseline.
Add a role-based system prompt function
Role prompting is the simplest and most impactful system prompt technique. A single sentence telling Claude who it is changes the tone, depth, and focus of every response.
- In prompt_workshop.py, find the run_baseline() function. Add the following new function directly below it:
def run_v1_role():
"""Version 1: Role and context only."""
system_prompt = """You are a senior security-focused code reviewer with 10 years of experience
in Python backend development. You review code for security vulnerabilities,
performance issues, and best practices violations. Keep your reviews concise
and prioritize the most critical issues first."""
print("=" * 60)
print("VERSION 1: Role + Context")
print("=" * 60)
response = call_claude(system_prompt, USER_MESSAGE)
print(response)
print()
return response
What does this system prompt do?
This four-sentence system prompt does three things that shape Claude's behavior:
- Defines a role (senior security-focused code reviewer). This narrows Claude's perspective to security expertise.
- Sets scope (security vulnerabilities, performance, best practices). Claude now knows exactly which categories to look for.
- Adds constraints (concise, critical issues first). This prevents long-winded responses and forces prioritization.
Why does role prompting work so well?
Anthropic's documentation states that setting a role in the system prompt focuses Claude's behavior and tone for your use case. Even a single sentence makes a difference.
Without a role, Claude tries to be helpful in a general way. With a role, it draws on the patterns associated with that expertise and delivers responses that match the perspective you defined.
- Save prompt_workshop.py.
Seeing a syntax error on the system prompt string?
Make sure you're using triple quotes (""") to open and close the multi-line string. Single or double quotes alone will break on the line wraps.
Also check that the function is indented at the top level (no extra indentation) and that it sits below run_baseline() but above main().
Help me fix my run_v1_role function
Run version 1 and compare to your baseline
The function exists but main() doesn't call it yet. You need to add one line to wire it up.
- In prompt_workshop.py, scroll down to the main() function.
- Find this line:
results["baseline"] = run_baseline()
- Add this line directly below it:
results["v1_role"] = run_v1_role()
- Save prompt_workshop.py.
✔️ Awesome, I've got everything!
Great. Double check you've saved your file, then move on to running the script.
ⓧ I'd like to double check the full code
import anthropic
import os
client = anthropic.Anthropic()
# The same user message we'll test against all system prompt versions
USER_MESSAGE = """
Please review this Python code:
def get_user_data(user_id):
query = f"SELECT * FROM users WHERE id = {user_id}"
result = db.execute(query)
password = result['password']
return {"id": result['id'], "name": result['name'], "email": result['email'], "password": password}
"""
MODEL = "claude-haiku-4-5"
def call_claude(system_prompt, user_message):
"""Send a message to Claude with the given system prompt."""
message = client.messages.create(
model=MODEL,
max_tokens=1024,
system=system_prompt if system_prompt else "",
messages=[
{"role": "user", "content": user_message}
],
)
return message.content[0].text
def run_baseline():
"""Version 0: No system prompt at all."""
print("=" * 60)
print("VERSION 0: No System Prompt (Baseline)")
print("=" * 60)
response = call_claude("", USER_MESSAGE)
print(response)
print()
return response
def run_v1_role():
"""Version 1: Role and context only."""
system_prompt = """You are a senior security-focused code reviewer with 10 years of experience
in Python backend development. You review code for security vulnerabilities,
performance issues, and best practices violations. Keep your reviews concise
and prioritize the most critical issues first."""
print("=" * 60)
print("VERSION 1: Role + Context")
print("=" * 60)
response = call_claude(system_prompt, USER_MESSAGE)
print(response)
print()
return response
def main():
print("\nSYSTEM PROMPT WORKSHOP")
print("Testing the same code review request with 4 different system prompts\n")
results = {}
results["baseline"] = run_baseline()
results["v1_role"] = run_v1_role()
if __name__ == "__main__":
main()
- Run the script by running this command:
python prompt_workshop.py
You should see two outputs printed one after the other. VERSION 0 (your baseline) prints first, followed by VERSION 1: Role + Context.
What should I see?
Compare the two outputs. Notice how version 1 differs from the baseline:
- Tone shifts from general helpfulness to authoritative expertise.
- Focus narrows to security vulnerabilities rather than covering every possible improvement.
- Priority is clearer. Critical issues (like the SQL injection) should appear first.
The SQL injection and password exposure in your test code are genuine security vulnerabilities. A good security-focused review should flag both prominently.
Both outputs look the same?
Check that you added results["v1_role"] = run_v1_role() inside main(). If you only defined the function without calling it, only the baseline will run.
Also confirm that the system_prompt variable inside run_v1_role() is not an empty string. It should contain the role description.
Why do my VERSION 0 and VERSION 1 outputs look identical?
You just proved that a four-sentence system prompt fundamentally changes Claude's output. Next up, you'll add structure and constraints to make the output predictable and parseable.
Add Output Format and Constraints
Version 1 gave Claude a role, and you saw how the tone and focus improved compared to the baseline. But the output structure was still unpredictable.
In this step, you'll add explicit format rules and behavioral constraints using XML tags. This makes Claude's output consistent and parseable every time you run it.
In this step, get ready to:
- Structure your system prompt with XML tags for clarity.
- Add output format rules and behavioral constraints.
- Run the script and compare version 2 against your earlier outputs.
Write the v2 system prompt function
Your v1 prompt told Claude what role to play, but it never said how to structure its answer. Different runs could produce wildly different formats.
Version 2 solves this by wrapping the prompt in XML tags that separate the role, output format, and constraints into distinct sections.
Why XML tags instead of Markdown?
Anthropic specifically recommends XML tags for structuring prompts sent to Claude. The opening and closing tags create unambiguous boundaries between sections. This makes it easier for Claude to parse your instructions and easier for you to maintain the prompt over time.
- In prompt_workshop.py, add the following function below run_v1_role():
def run_v2_format():
"""Version 2: Role + format + constraints."""
system_prompt = """<role>
You are a senior security-focused code reviewer with 10 years of experience
in Python backend development.
</role>
<output_format>
Structure every review as follows:
1. CRITICAL issues (security vulnerabilities, data exposure)
2. WARNING issues (performance, maintainability)
3. SUGGESTION (style, best practices)
For each issue, provide:
- Line reference or code snippet
- What is wrong
- Why it matters
- How to fix it (concrete code example)
</output_format>
<constraints>
- Never suggest a rewrite without explaining the specific vulnerability or issue it addresses.
- Always mention security implications when they exist.
- Limit your review to the 3 most important issues maximum.
- Use severity labels: [CRITICAL], [WARNING], [SUGGESTION].
</constraints>"""
print("=" * 60)
print("VERSION 2: Role + Format + Constraints")
print("=" * 60)
response = call_claude(system_prompt, USER_MESSAGE)
print(response)
print()
return response
What does this system prompt do?
This prompt has three distinct XML sections, each with a clear job:
- <role> carries the same senior reviewer identity from v1, now cleanly separated from other instructions.
- <output_format> tells Claude exactly how to structure its answer. It defines a three-tier severity system and requires four pieces of information per issue.
- <constraints> sets hard behavioral rules. Claude must explain every suggestion, highlight security implications, and limit output to 3 issues maximum.
- Save prompt_workshop.py.
- You won't see a change yet. The next step hooks this function into main().
Call the new function from main
- In prompt_workshop.py, scroll down to your main() function.
- Find this line:
results["v1_role"] = run_v1_role()
- Add this line directly below it:
results["v2_format"] = run_v2_format()
- Save prompt_workshop.py.
✔️ Awesome, I've got everything!
Great. Double check you've saved your file before running.
ⓧ I'd like to double check the full code
import anthropic
import os
client = anthropic.Anthropic()
# The same user message we'll test against all system prompt versions
USER_MESSAGE = """
Please review this Python code:
def get_user_data(user_id):
query = f"SELECT * FROM users WHERE id = {user_id}"
result = db.execute(query)
password = result['password']
return {"id": result['id'], "name": result['name'], "email": result['email'], "password": password}
"""
MODEL = "claude-haiku-4-5"
def call_claude(system_prompt, user_message):
"""Send a message to Claude with the given system prompt."""
message = client.messages.create(
model=MODEL,
max_tokens=1024,
system=system_prompt if system_prompt else "",
messages=[
{"role": "user", "content": user_message}
],
)
return message.content[0].text
def run_baseline():
"""Version 0: No system prompt at all."""
print("=" * 60)
print("VERSION 0: No System Prompt (Baseline)")
print("=" * 60)
response = call_claude("", USER_MESSAGE)
print(response)
print()
return response
def run_v1_role():
"""Version 1: Role and context only."""
system_prompt = """You are a senior security-focused code reviewer with 10 years of experience
in Python backend development. You review code for security vulnerabilities,
performance issues, and best practices violations. Keep your reviews concise
and prioritize the most critical issues first."""
print("=" * 60)
print("VERSION 1: Role + Context")
print("=" * 60)
response = call_claude(system_prompt, USER_MESSAGE)
print(response)
print()
return response
def run_v2_format():
"""Version 2: Role + format + constraints."""
system_prompt = """<role>
You are a senior security-focused code reviewer with 10 years of experience
in Python backend development.
</role>
<output_format>
Structure every review as follows:
1. CRITICAL issues (security vulnerabilities, data exposure)
2. WARNING issues (performance, maintainability)
3. SUGGESTION (style, best practices)
For each issue, provide:
- Line reference or code snippet
- What is wrong
- Why it matters
- How to fix it (concrete code example)
</output_format>
<constraints>
- Never suggest a rewrite without explaining the specific vulnerability or issue it addresses.
- Always mention security implications when they exist.
- Limit your review to the 3 most important issues maximum.
- Use severity labels: [CRITICAL], [WARNING], [SUGGESTION].
</constraints>"""
print("=" * 60)
print("VERSION 2: Role + Format + Constraints")
print("=" * 60)
response = call_claude(system_prompt, USER_MESSAGE)
print(response)
print()
return response
def main():
print("\nSYSTEM PROMPT WORKSHOP")
print("Testing the same code review request with 4 different system prompts\n")
results = {}
results["baseline"] = run_baseline()
results["v1_role"] = run_v1_role()
results["v2_format"] = run_v2_format()
if __name__ == "__main__":
main()
Run the script and compare outputs
Now you can see version 2 in action alongside your earlier prompts.
- Run your script by entering this command in the terminal:
python prompt_workshop.py
- Scroll through the output. Under the VERSION 2: Role + Format + Constraints header, you should see a structured review with severity labels like [CRITICAL], [WARNING], or [SUGGESTION].
- Compare it to version 1. Notice that version 2 limits itself to 3 issues, uses consistent labels, and includes a concrete fix for each problem.
What changed between v1 and v2?
Version 1 told Claude what role to play but left the output format entirely up to it. Every run could produce a different structure.
Version 2 locks in the format. The <output_format> section means you get predictable, parseable output. The <constraints> section means Claude follows behavioral rules (max 3 issues, always explain why). This is what makes a system prompt production-ready.
Output still looks unstructured?
Make sure your XML tags are spelled exactly as shown. A typo like <output-format> instead of <output_format> can confuse the parser.
Check that the triple-quoted string starts with """<role> on the same line. Extra whitespace before the first XML tag can be ignored by Claude, but mismatched quotes will cause a Python syntax error.
help me debug why my v2 system prompt output doesn't have severity labels or structure
Your system prompt now produces consistent, structured output every time. Next up, you'll add few-shot examples that show Claude exactly what a perfect review looks like.
Add Few-Shot Examples
Your v2 system prompt now produces structured, predictable output. Claude follows the format and respects the constraints. But there is still room for inconsistency in the depth and style of each issue.
Few-shot examples solve this. By showing Claude exactly what a perfect review looks like, you lock in the tone, depth, and structure you want. Think of it as showing someone a completed form instead of just describing the fields.
In this step, get ready to:
- Add a few-shot example to your system prompt showing a model code review.
- Run the updated prompt and compare against all previous versions.
Add a few-shot example to your system prompt
Few-shot examples are concrete input/output pairs embedded in the system prompt. They show the model exactly what a good response looks like, rather than relying on descriptions alone.
What are few-shot examples?
Anthropic's documentation states that examples are one of the most powerful ways to steer Claude's output format and structure. Instead of telling Claude "write detailed explanations," you show it a complete example of what "detailed" means in your context.
Even a single well-crafted example can dramatically improve consistency across responses.
Your example will demonstrate reviewing a login function with multiple vulnerabilities. This gives Claude a reference point for how deep to go, what tone to use, and how to structure each issue.
- In prompt_workshop.py, add this new function below your run_v2_format() function:
def run_v3_examples():
"""Version 3: Role + format + constraints + few-shot examples."""
system_prompt = """<role>
You are a senior security-focused code reviewer with 10 years of experience
in Python backend development.
</role>
<output_format>
Structure every review as follows:
1. CRITICAL issues (security vulnerabilities, data exposure)
2. WARNING issues (performance, maintainability)
3. SUGGESTION (style, best practices)
For each issue, provide:
- Line reference or code snippet
- What is wrong
- Why it matters
- How to fix it (concrete code example)
</output_format>
<constraints>
- Never suggest a rewrite without explaining the specific vulnerability or issue it addresses.
- Always mention security implications when they exist.
- Limit your review to the 3 most important issues maximum.
- Use severity labels: [CRITICAL], [WARNING], [SUGGESTION].
</constraints>
<examples>
<example>
<user_code>
def login(username, password):
user = db.query(f"SELECT * FROM users WHERE username = '{username}'")
if user and user.password == password:
return create_session(user)
</user_code>
<review>
[CRITICAL] SQL Injection vulnerability
`f"SELECT * FROM users WHERE username = '{username}'"` directly interpolates user input into SQL.
An attacker can pass `' OR '1'='1` as the username to bypass authentication entirely and access any account.
Fix: Use parameterized queries.
```python
user = db.query("SELECT * FROM users WHERE username = %s", (username,))
```
[CRITICAL] Plain-text password comparison
`user.password == password` suggests passwords are stored in plain text.
If the database is compromised, all user passwords are immediately exposed. This also violates most compliance standards (SOC 2, GDPR).
Fix: Use bcrypt or argon2 for password hashing.
```python
import bcrypt
if user and bcrypt.checkpw(password.encode(), user.password_hash):
return create_session(user)
```
[WARNING] No rate limiting or failed attempt tracking
Repeated failed logins are not tracked or limited.
An attacker can brute-force passwords without restriction.
Fix: Add rate limiting with a counter and lockout threshold.
</review>
</example>
</examples>"""
print("=" * 60)
print("VERSION 3: Role + Format + Constraints + Examples")
print("=" * 60)
response = call_claude(system_prompt, USER_MESSAGE)
print(response)
print()
return response
What does this code do?
This function builds on the v2 system prompt by adding an <examples> XML section at the end. The example contains two parts:
- <user_code> shows a login function with SQL injection, plain-text password storage, and missing rate limiting. This demonstrates handling code with both obvious and subtle vulnerabilities.
- <review> shows the exact format, tone, depth, and code fixes you want Claude to replicate in every review.
Claude uses this example as a template, matching its style and depth when it reviews your actual code.
Update main() to call the new version
- In the main() function, find this line:
results["v2_format"] = run_v2_format()
- Add this line directly below it:
results["v3_examples"] = run_v3_examples()
- Save prompt_workshop.py.
Run the script and compare all versions
Now you can see how the few-shot example locks in exactly the format and depth you demonstrated. The same user message goes through all four versions.
- Run your script by executing this command:
python prompt_workshop.py
You should see four outputs printed in sequence. The VERSION 3 output closely mirrors the style, tone, and structure of the example you provided in the system prompt.
What should I notice in the v3 output?
Compare VERSION 3 against your earlier versions. The v3 output should match the example's pattern closely. It uses the same severity labels, provides explanations before fixes, and gives concrete code snippets for each fix.
The few-shot example "teaches" Claude by demonstration. Where v2 described the format in rules, v3 shows it in action. This is why examples are so powerful for controlling AI output.
Seeing an error or unexpected output?
- If you see a SyntaxError, check that your triple-quoted string uses three double quotes on both ends and that any internal quotes are not breaking the string.
- If the output looks identical to v2, make sure you added the <examples> section after the </constraints> closing tag, not inside it.
- If you get a NameError for run_v3_examples, check that the function is defined above main() in your file.
Why is my v3 output not matching the example format?
✔️ Awesome, I've got everything!
Great. Double check you have saved your file before moving on.
ⓧ I'd like to double check the full code
Here is the full prompt_workshop.py as it should look at the end of this step:
import anthropic
import os
client = anthropic.Anthropic()
# The same user message we'll test against all system prompt versions
USER_MESSAGE = """
Please review this Python code:
def get_user_data(user_id):
query = f"SELECT * FROM users WHERE id = {user_id}"
result = db.execute(query)
password = result['password']
return {"id": result['id'], "name": result['name'], "email": result['email'], "password": password}
"""
MODEL = "claude-haiku-4-5"
def call_claude(system_prompt, user_message):
"""Send a message to Claude with the given system prompt."""
message = client.messages.create(
model=MODEL,
max_tokens=1024,
system=system_prompt if system_prompt else "",
messages=[
{"role": "user", "content": user_message}
],
)
return message.content[0].text
def run_baseline():
"""Version 0: No system prompt at all."""
print("=" * 60)
print("VERSION 0: No System Prompt (Baseline)")
print("=" * 60)
response = call_claude("", USER_MESSAGE)
print(response)
print()
return response
def run_v1_role():
"""Version 1: Role and context only."""
system_prompt = """You are a senior security-focused code reviewer with 10 years of experience
in Python backend development. You review code for security vulnerabilities,
performance issues, and best practices violations. Keep your reviews concise
and prioritize the most critical issues first."""
print("=" * 60)
print("VERSION 1: Role + Context")
print("=" * 60)
response = call_claude(system_prompt, USER_MESSAGE)
print(response)
print()
return response
def run_v2_format():
"""Version 2: Role + format + constraints."""
system_prompt = """<role>
You are a senior security-focused code reviewer with 10 years of experience
in Python backend development.
</role>
<output_format>
Structure every review as follows:
1. CRITICAL issues (security vulnerabilities, data exposure)
2. WARNING issues (performance, maintainability)
3. SUGGESTION (style, best practices)
For each issue, provide:
- Line reference or code snippet
- What is wrong
- Why it matters
- How to fix it (concrete code example)
</output_format>
<constraints>
- Never suggest a rewrite without explaining the specific vulnerability or issue it addresses.
- Always mention security implications when they exist.
- Limit your review to the 3 most important issues maximum.
- Use severity labels: [CRITICAL], [WARNING], [SUGGESTION].
</constraints>"""
print("=" * 60)
print("VERSION 2: Role + Format + Constraints")
print("=" * 60)
response = call_claude(system_prompt, USER_MESSAGE)
print(response)
print()
return response
def run_v3_examples():
"""Version 3: Role + format + constraints + few-shot examples."""
system_prompt = """<role>
You are a senior security-focused code reviewer with 10 years of experience
in Python backend development.
</role>
<output_format>
Structure every review as follows:
1. CRITICAL issues (security vulnerabilities, data exposure)
2. WARNING issues (performance, maintainability)
3. SUGGESTION (style, best practices)
For each issue, provide:
- Line reference or code snippet
- What is wrong
- Why it matters
- How to fix it (concrete code example)
</output_format>
<constraints>
- Never suggest a rewrite without explaining the specific vulnerability or issue it addresses.
- Always mention security implications when they exist.
- Limit your review to the 3 most important issues maximum.
- Use severity labels: [CRITICAL], [WARNING], [SUGGESTION].
</constraints>
<examples>
<example>
<user_code>
def login(username, password):
user = db.query(f"SELECT * FROM users WHERE username = '{username}'")
if user and user.password == password:
return create_session(user)
</user_code>
<review>
[CRITICAL] SQL Injection vulnerability
`f"SELECT * FROM users WHERE username = '{username}'"` directly interpolates user input into SQL.
An attacker can pass `' OR '1'='1` as the username to bypass authentication entirely and access any account.
Fix: Use parameterized queries.
```python
user = db.query("SELECT * FROM users WHERE username = %s", (username,))
```
[CRITICAL] Plain-text password comparison
`user.password == password` suggests passwords are stored in plain text.
If the database is compromised, all user passwords are immediately exposed. This also violates most compliance standards (SOC 2, GDPR).
Fix: Use bcrypt or argon2 for password hashing.
```python
import bcrypt
if user and bcrypt.checkpw(password.encode(), user.password_hash):
return create_session(user)
```
[WARNING] No rate limiting or failed attempt tracking
Repeated failed logins are not tracked or limited.
An attacker can brute-force passwords without restriction.
Fix: Add rate limiting with a counter and lockout threshold.
</review>
</example>
</examples>"""
print("=" * 60)
print("VERSION 3: Role + Format + Constraints + Examples")
print("=" * 60)
response = call_claude(system_prompt, USER_MESSAGE)
print(response)
print()
return response
def main():
print("\nSYSTEM PROMPT WORKSHOP")
print("Testing the same code review request with 4 different system prompts\n")
results = {}
results["baseline"] = run_baseline()
results["v1_role"] = run_v1_role()
results["v2_format"] = run_v2_format()
results["v3_examples"] = run_v3_examples()
if __name__ == "__main__":
main()
You now have a system prompt that uses all four major techniques: role, format, constraints, and examples. In the next step, you will combine everything into a final comparison script and save your polished system prompt for reuse in future projects.
Run the Full Comparison
You now have four system prompt versions that progressively refine Claude's code review behavior. Your script already calls all four functions and prints each output with clear VERSION labels.
In this final step, you will add code to export your best system prompt to a reusable file. Then you will run the full script end-to-end and see all four outputs together, proving how much each technique matters.
In this step, get ready to:
- Add code that saves your final system prompt to a reusable file.
- Run the full comparison and see all 4 system prompt versions side by side.
- Verify that your exported system prompt file is ready for future projects.
Save your final system prompt to a file
A good system prompt is an asset you can reuse across projects. By saving your final version to a standalone .txt file, you can load it into any script without copying the entire string every time.
- In prompt_workshop.py, find the last line inside your main() function. It should be:
results["v3_examples"] = run_v3_examples()
- Add the following code directly below that line, still inside main():
# Save the final system prompt to a file
final_prompt = """<role>
You are a senior security-focused code reviewer with 10 years of experience
in Python backend development.
</role>
<output_format>
Structure every review as follows:
1. CRITICAL issues (security vulnerabilities, data exposure)
2. WARNING issues (performance, maintainability)
3. SUGGESTION (style, best practices)
For each issue, provide:
- Line reference or code snippet
- What is wrong
- Why it matters
- How to fix it (concrete code example)
</output_format>
<constraints>
- Never suggest a rewrite without explaining the specific vulnerability or issue it addresses.
- Always mention security implications when they exist.
- Limit your review to the 3 most important issues maximum.
- Use severity labels: [CRITICAL], [WARNING], [SUGGESTION].
</constraints>
<examples>
<example>
<user_code>
def login(username, password):
user = db.query(f"SELECT * FROM users WHERE username = '{username}'")
if user and user.password == password:
return create_session(user)
</user_code>
<review>
[CRITICAL] SQL Injection vulnerability
`f"SELECT * FROM users WHERE username = '{username}'"` directly interpolates user input into SQL.
An attacker can pass `' OR '1'='1` as the username to bypass authentication entirely and access any account.
Fix: Use parameterized queries.
```python
user = db.query("SELECT * FROM users WHERE username = %s", (username,))
```
[CRITICAL] Plain-text password comparison
`user.password == password` suggests passwords are stored in plain text.
If the database is compromised, all user passwords are immediately exposed. This also violates most compliance standards (SOC 2, GDPR).
Fix: Use bcrypt or argon2 for password hashing.
```python
import bcrypt
if user and bcrypt.checkpw(password.encode(), user.password_hash):
return create_session(user)
```
[WARNING] No rate limiting or failed attempt tracking
Repeated failed logins are not tracked or limited.
An attacker can brute-force passwords without restriction.
Fix: Add rate limiting with a counter and lockout threshold.
</review>
</example>
</examples>"""
with open("system_prompt.txt", "w") as f:
f.write(final_prompt)
print("=" * 60)
print("DONE! Final system prompt saved to system_prompt.txt")
print("=" * 60)
What does this code do?
This code defines final_prompt as a string containing your complete v3 system prompt. This is the same prompt from run_v3_examples() with all four sections: role, output format, constraints, and examples.
The open("system_prompt.txt", "w") call creates a new file and writes your prompt into it. Now you have a standalone file you can load into any future Python script that needs a code review prompt.
- Save prompt_workshop.py.
Run the full comparison
Now you will run the complete script from start to finish. This sends the same code review request through all four system prompts in sequence, so you can see the progression from generic to production-quality.
- Run the full workshop script by entering this command in your terminal:
python prompt_workshop.py
- Watch all four outputs print in sequence. Each one is labeled with its VERSION number and the techniques it uses.
- At the very end, you should see DONE! Final system prompt saved to system_prompt.txt printed in the terminal.
What should I see?
You will see four clearly separated outputs, each between rows of = characters. Compare them as they print:
- VERSION 0 (Baseline) gives a generic, unstructured response. It might ramble or miss key issues.
- VERSION 1 (Role + Context) shifts the tone to a security reviewer. The focus tightens.
- VERSION 2 (Format + Constraints) locks in a predictable structure with severity labels.
- VERSION 3 (Examples) produces output that closely mirrors your example format. This is the most consistent and production-ready.
- Confirm that system_prompt.txt was created by running:
ls
- You should see system_prompt.txt listed alongside prompt_workshop.py in the output.
Don't see system_prompt.txt?
- Make sure you saved prompt_workshop.py before running the script. Unsaved changes will not execute.
- Check that the new code is inside the main() function (indented with 4 spaces), not at the top level of the file.
- Look at the terminal output for any error messages. A missing quote or bracket in final_prompt will cause a syntax error before the file gets written.
Why isn't my system_prompt.txt file being created when I run the script?
✔️ Awesome, I've got everything!
Great. Double check you have saved prompt_workshop.py and that system_prompt.txt exists in your project folder.
ⓧ I'd like to double check the full code
import anthropic
import os
client = anthropic.Anthropic()
# The same user message we'll test against all system prompt versions
USER_MESSAGE = """
Please review this Python code:
def get_user_data(user_id):
query = f"SELECT * FROM users WHERE id = {user_id}"
result = db.execute(query)
password = result['password']
return {"id": result['id'], "name": result['name'], "email": result['email'], "password": password}
"""
MODEL = "claude-haiku-4-5"
def call_claude(system_prompt, user_message):
"""Send a message to Claude with the given system prompt."""
message = client.messages.create(
model=MODEL,
max_tokens=1024,
system=system_prompt if system_prompt else "",
messages=[
{"role": "user", "content": user_message}
],
)
return message.content[0].text
def run_baseline():
"""Version 0: No system prompt at all."""
print("=" * 60)
print("VERSION 0: No System Prompt (Baseline)")
print("=" * 60)
response = call_claude("", USER_MESSAGE)
print(response)
print()
return response
def run_v1_role():
"""Version 1: Role and context only."""
system_prompt = """You are a senior security-focused code reviewer with 10 years of experience
in Python backend development. You review code for security vulnerabilities,
performance issues, and best practices violations. Keep your reviews concise
and prioritize the most critical issues first."""
print("=" * 60)
print("VERSION 1: Role + Context")
print("=" * 60)
response = call_claude(system_prompt, USER_MESSAGE)
print(response)
print()
return response
def run_v2_format():
"""Version 2: Role + format + constraints."""
system_prompt = """<role>
You are a senior security-focused code reviewer with 10 years of experience
in Python backend development.
</role>
<output_format>
Structure every review as follows:
1. CRITICAL issues (security vulnerabilities, data exposure)
2. WARNING issues (performance, maintainability)
3. SUGGESTION (style, best practices)
For each issue, provide:
- Line reference or code snippet
- What is wrong
- Why it matters
- How to fix it (concrete code example)
</output_format>
<constraints>
- Never suggest a rewrite without explaining the specific vulnerability or issue it addresses.
- Always mention security implications when they exist.
- Limit your review to the 3 most important issues maximum.
- Use severity labels: [CRITICAL], [WARNING], [SUGGESTION].
</constraints>"""
print("=" * 60)
print("VERSION 2: Role + Format + Constraints")
print("=" * 60)
response = call_claude(system_prompt, USER_MESSAGE)
print(response)
print()
return response
def run_v3_examples():
"""Version 3: Role + format + constraints + few-shot examples."""
system_prompt = """<role>
You are a senior security-focused code reviewer with 10 years of experience
in Python backend development.
</role>
<output_format>
Structure every review as follows:
1. CRITICAL issues (security vulnerabilities, data exposure)
2. WARNING issues (performance, maintainability)
3. SUGGESTION (style, best practices)
For each issue, provide:
- Line reference or code snippet
- What is wrong
- Why it matters
- How to fix it (concrete code example)
</output_format>
<constraints>
- Never suggest a rewrite without explaining the specific vulnerability or issue it addresses.
- Always mention security implications when they exist.
- Limit your review to the 3 most important issues maximum.
- Use severity labels: [CRITICAL], [WARNING], [SUGGESTION].
</constraints>
<examples>
<example>
<user_code>
def login(username, password):
user = db.query(f"SELECT * FROM users WHERE username = '{username}'")
if user and user.password == password:
return create_session(user)
</user_code>
<review>
[CRITICAL] SQL Injection vulnerability
`f"SELECT * FROM users WHERE username = '{username}'"` directly interpolates user input into SQL.
An attacker can pass `' OR '1'='1` as the username to bypass authentication entirely and access any account.
Fix: Use parameterized queries.
```python
user = db.query("SELECT * FROM users WHERE username = %s", (username,))
```
[CRITICAL] Plain-text password comparison
`user.password == password` suggests passwords are stored in plain text.
If the database is compromised, all user passwords are immediately exposed. This also violates most compliance standards (SOC 2, GDPR).
Fix: Use bcrypt or argon2 for password hashing.
```python
import bcrypt
if user and bcrypt.checkpw(password.encode(), user.password_hash):
return create_session(user)
```
[WARNING] No rate limiting or failed attempt tracking
Repeated failed logins are not tracked or limited.
An attacker can brute-force passwords without restriction.
Fix: Add rate limiting with a counter and lockout threshold.
</review>
</example>
</examples>"""
print("=" * 60)
print("VERSION 3: Role + Format + Constraints + Examples")
print("=" * 60)
response = call_claude(system_prompt, USER_MESSAGE)
print(response)
print()
return response
def main():
print("\nSYSTEM PROMPT WORKSHOP")
print("Testing the same code review request with 4 different system prompts\n")
results = {}
results["baseline"] = run_baseline()
results["v1_role"] = run_v1_role()
results["v2_format"] = run_v2_format()
results["v3_examples"] = run_v3_examples()
# Save the final system prompt to a file
final_prompt = """<role>
You are a senior security-focused code reviewer with 10 years of experience
in Python backend development.
</role>
<output_format>
Structure every review as follows:
1. CRITICAL issues (security vulnerabilities, data exposure)
2. WARNING issues (performance, maintainability)
3. SUGGESTION (style, best practices)
For each issue, provide:
- Line reference or code snippet
- What is wrong
- Why it matters
- How to fix it (concrete code example)
</output_format>
<constraints>
- Never suggest a rewrite without explaining the specific vulnerability or issue it addresses.
- Always mention security implications when they exist.
- Limit your review to the 3 most important issues maximum.
- Use severity labels: [CRITICAL], [WARNING], [SUGGESTION].
</constraints>
<examples>
<example>
<user_code>
def login(username, password):
user = db.query(f"SELECT * FROM users WHERE username = '{username}'")
if user and user.password == password:
return create_session(user)
</user_code>
<review>
[CRITICAL] SQL Injection vulnerability
`f"SELECT * FROM users WHERE username = '{username}'"` directly interpolates user input into SQL.
An attacker can pass `' OR '1'='1` as the username to bypass authentication entirely and access any account.
Fix: Use parameterized queries.
```python
user = db.query("SELECT * FROM users WHERE username = %s", (username,))
```
[CRITICAL] Plain-text password comparison
`user.password == password` suggests passwords are stored in plain text.
If the database is compromised, all user passwords are immediately exposed. This also violates most compliance standards (SOC 2, GDPR).
Fix: Use bcrypt or argon2 for password hashing.
```python
import bcrypt
if user and bcrypt.checkpw(password.encode(), user.password_hash):
return create_session(user)
```
[WARNING] No rate limiting or failed attempt tracking
Repeated failed logins are not tracked or limited.
An attacker can brute-force passwords without restriction.
Fix: Add rate limiting with a counter and lockout threshold.
</review>
</example>
</examples>"""
with open("system_prompt.txt", "w") as f:
f.write(final_prompt)
print("=" * 60)
print("DONE! Final system prompt saved to system_prompt.txt")
print("=" * 60)
if __name__ == "__main__":
main()
Why save the prompt to a separate file?
In real projects, you would load the system prompt from a file rather than hardcoding it in your script. This lets you version-control your prompts separately, swap them out without touching code, and share them across different scripts or team members.
You now have a complete System Prompt Workshop that proves how each prompting technique transforms AI output. Your system_prompt.txt file is ready to drop into any future project that needs a code review assistant. Up next: a Secret Mission that takes your workshop to the next level with automated scoring.
Secret mission
Build a Prompt Scoring Harness
You have four system prompt versions producing different outputs. But which one is actually best? Build an automated scoring harness that uses Claude itself as the judge, rating each version against a quality rubric. This is the LLM-as-judge pattern used in production prompt engineering.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally and only costs money when you actively run the script. There are no ongoing charges once you stop running it.
Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- Your project folder on the Desktop (contains prompt_workshop.py and system_prompt.txt).
- The anthropic Python package (installed via pip).
- The ANTHROPIC_API_KEY environment variable.
- Your API key in the Anthropic Console.
Keep everything running
No action needed. Choose this if you are still actively building or plan to extend this project soon.
- Your project files stay on your Desktop.
- Your ANTHROPIC_API_KEY environment variable remains set for future API calls.
- Your API key stays active in the Anthropic Console.
- The anthropic package stays installed in your Python environment.
Pause - I'll come back to this later
Remove the environment variable so the key is not exposed in your terminal sessions. Keep your files for later.
- Remove the ANTHROPIC_API_KEY from your current session by running:
- On macOS/Linux:
unset ANTHROPIC_API_KEY
- On Windows (PowerShell):
Remove-Item Env:ANTHROPIC_API_KEY
- If you added the key to your shell profile (~/.zshrc or ~/.bashrc), remove that line as well.
- Your project folder and installed packages stay on your machine for when you come back.
Delete - I don't want to use this again
Remove all project resources and start fresh.
- Delete your project folder. On macOS/Linux, run:
rm -rf ~/Desktop/prompt-workshop
- On Windows (PowerShell), run:
Remove-Item -Recurse -Force ~\Desktop\prompt-workshop
- Uninstall the anthropic package by running:
pip uninstall anthropic
- Remove the ANTHROPIC_API_KEY environment variable from your current session. On macOS/Linux:
unset ANTHROPIC_API_KEY
- On Windows (PowerShell):
Remove-Item Env:ANTHROPIC_API_KEY
- If you added the key to your shell profile (~/.zshrc or ~/.bashrc), remove that line as well.
- Disable your API key in the Anthropic Console. Navigate to your API keys page at console.anthropic.com/account/keys and disable or delete the key you created for this project.
That's a Wrap!
That's a Wrap!
Nice work! You just built a System Prompt Workshop that proves the single highest-impact skill in AI development.
Here's what you accomplished:
- Built a Python script that calls the Claude API with custom system prompts, transforming generic responses into focused, structured code reviews.
- Created 4 progressively refined system prompts demonstrating the impact of role prompting, behavioral constraints, output formatting, and few-shot examples.
- Produced a reusable system prompt template saved to a separate file that you can adapt for any future AI project.
- Secret Mission: Built an automated prompt scoring harness using the LLM-as-judge pattern to evaluate system prompt quality.
Ready to quiz yourself?