Set a System Prompt

Su Suriya Ravichandran Updated 10 Nov 2026
10 min read ·Lesson 32 of 200

From Mechanics to Design

Lesson 17 explained how system prompts work in the SDK: the system parameter, its priority over user messages, and its string and block forms. This lesson is about designing a good system prompt: what to put in it, how to structure it, how to control tone and format, and how to maintain and test it like any other important part of an application.

A system prompt is effectively the job description for Claude in your application. A precise job description produces consistent, useful behavior. A vague one produces answers that vary from request to request.


Give Claude a Role

The simplest effective technique is to set a role. According to Anthropic's prompting guidance, even a single sentence of role-setting in the system prompt focuses Claude's behavior and tone for your use case.

message = client.messages.create(
    model="claude-opus-5-5",
    max_tokens=2048,
    system="You are a senior Django developer who reviews code written by junior developers.",
    messages=[{"role": "user", "content": "Is it okay to put business logic in my views?"}],
)

Why a role helps: a role implies expertise, audience, and perspective all at once. "Senior Django developer reviewing juniors' code" tells Claude what level of knowledge to apply, which conventions to follow, and how to frame advice: constructive, practical, and teaching-oriented.

Make Roles Specific

Generic roleSpecific role
"You are a helpful assistant.""You are a support assistant for LearnPy, an online platform for self-paced Python courses."
"You are a writer.""You are a technical writer who creates W3Schools-style tutorials for beginner developers."
"You are an expert.""You are a security engineer reviewing Python web applications for OWASP Top 10 risks."

The specific roles carry far more information: the domain, the audience, and the standards to apply.


The Anatomy of a Strong System Prompt

A production system prompt usually combines these sections. Not every application needs all of them.

SectionPurposeExample content
RoleWho Claude is"You are the study assistant for GoCourse's Python track."
GoalWhat success looks like"Help students understand concepts, not just get answers."
AudienceWho reads the output"Students are beginners, many studying on phones."
Knowledge and contextFacts Claude needsCourse structure, policies, product details
Behavior rulesWhat to do and not do"When a student shares exercise code, give hints before full solutions."
Tone and styleHow to communicate"Friendly and encouraging; plain language."
Output formatStructure and length"Under 150 words; code in fenced blocks."
Boundaries and escalationLimits and hand-offs"For billing questions, direct students to support@…"
Dynamic detailsPer-user or per-request data, placed lastStudent name, enrolled courses, today's date

A Structured Template

XML-style tags (Lesson 37) keep the sections distinct and easy to maintain:

You are the study assistant for GoCourse's Python track.

<goal>
Help students understand Python concepts so they can solve problems on their own.
Understanding matters more than speed, because students are graded on projects they write themselves.
</goal>

<audience>
Beginners aged 16 and up, often studying on phones in short sessions.
</audience>

<behavior>
- When a student shares exercise code, give a hint first. Give the full solution only if they ask again.
- When explaining a concept, use one short code example.
- If you are not sure about a course-specific detail, say so and suggest asking a mentor.
</behavior>

<style>
Friendly, encouraging, and plain. Short paragraphs. Code in fenced code blocks.
Keep answers under 150 words unless the student asks for more detail.
</style>

<boundaries>
For billing, refunds, or certificate problems, direct the student to support@gocourse.example.
</boundaries>

Why this structure works:

  • Each section has one job, so editing tone does not accidentally change policy.
  • Reasons are included where they matter ("because students are graded on projects…"), so Claude applies the rules sensibly (Lesson 31).
  • Uncertainty handling ("If you are not sure… say so") reduces confident guessing about facts Claude cannot know.

Controlling Tone and Format

Describe the Style Positively

Tell Claude what the output should look like rather than only what to avoid:

Write in clear, flowing paragraphs. Use a numbered list only when describing steps
that must happen in order.

This works better than "Don't use bullet points", because it describes the target.

Match the Prompt's Style to the Output You Want

The style of the prompt itself influences the style of the response. A system prompt written as dense bullet points and headings tends to encourage similarly fragmented answers. A prompt written in flowing prose encourages prose. If you want conversational answers, write a conversational system prompt.

Be Explicit About Length

Current models default to concise, direct answers. If your application needs detailed explanations, say so in the system prompt. If it needs very short answers, give a concrete limit.


Identity and Product Context

If Claude should present itself in a particular way inside your product, state it:

You are "PyBuddy", the study assistant inside the GoCourse app, powered by Claude.
If students ask what you are, explain that you are an AI assistant and cannot see their
account details unless they are included in this conversation.

Why: without this, Claude does not know the product name, what it can access, or how to describe itself to users. Being transparent that the assistant is an AI, and about what it can and cannot see, builds appropriate user trust.


Static First, Dynamic Last

Place content that never changes at the beginning of the system prompt, and content that changes per user or request at the end:

STATIC_PROMPT = Path("prompts/study_assistant_v3.txt").read_text(encoding="utf-8")


def build_system(student_name: str, course: str) -> str:
    return (
        f"{STATIC_PROMPT}\n\n"
        f"<student>\nName: {student_name}\nCurrent course: {course}\n</student>"
    )

Why the order matters: prompt caching (Lessons 121–130) reuses the identical beginning of requests. A long, stable instruction block at the start can be cached and reused across all users. A per-user section at the end only affects the small part that follows it.


Treat System Prompts as Code

A system prompt determines your application's behavior, so manage it with the same care as code.

PracticeWhy
Store prompts in version-controlled filesChanges are reviewed, tracked, and reversible
Name versions (study_assistant_v3.txt)You know which version produced which behavior
Log the prompt version with each requestProblems can be traced to a specific prompt change
Test with a fixed set of cases before deploying changesA fix for one behavior can break another
Keep secrets outSystem prompts can potentially be revealed to users (Lesson 17)

Practical Example: A Versioned System Prompt with Regression Tests

This example stores a system prompt in a file, loads it by version, and runs a small set of regression tests: fixed inputs with automatic checks that confirm the prompt still behaves as intended.

Project Structure

study-assistant/
├── prompts/
│   └── study_assistant_v1.txt
└── check_prompt.py

prompts/study_assistant_v1.txt

You are PyBuddy, the study assistant for GoCourse's Python track, powered by Claude.

<behavior>
- When a student shares exercise code and asks for the answer, give a hint first, not the full solution.
- For billing, refund, or certificate questions, direct the student to support@gocourse.example.
- If you are not sure about a course-specific detail, say you are not sure.
</behavior>

<style>
Friendly and plain. Keep answers under 120 words. Put code in fenced code blocks.
</style>

check_prompt.py

"""check_prompt.py: run regression checks against a versioned system prompt."""

import sys
from pathlib import Path

from anthropic import Anthropic
from anthropic.types import Message

PROMPT_VERSION = "study_assistant_v1"
SYSTEM = (Path("prompts") / f"{PROMPT_VERSION}.txt").read_text(encoding="utf-8")
client = Anthropic()


def get_text(message: Message) -> str:
    return "".join(block.text for block in message.content if block.type == "text").strip()


CASES = [
    {
        "name": "billing questions are redirected",
        "input": "I was charged twice, can you refund me?",
        "check": lambda text: "support@gocourse.example" in text,
    },
    {
        "name": "exercise help starts with a hint",
        "input": "My loop prints 0 to 4 but should print 1 to 5:\nfor i in range(5): print(i)\nJust give me the fixed code.",
        "check": lambda text: "range(1, 6)" not in text,
    },
    {
        "name": "answers stay short",
        "input": "What is a Python list?",
        "check": lambda text: len(text.split()) <= 150,
    },
    {
        "name": "identifies as PyBuddy",
        "input": "What's your name?",
        "check": lambda text: "PyBuddy" in text,
    },
]


def run() -> int:
    failures = 0
    print(f"Prompt version: {PROMPT_VERSION}\n")
    for case in CASES:
        reply = client.messages.create(
            model="claude-opus-5-5",
            max_tokens=2048,
            system=SYSTEM,
            messages=[{"role": "user", "content": case["input"]}],
        )
        text = get_text(reply)
        passed = case["check"](text)
        failures += not passed
        print(f"[{'PASS' if passed else 'FAIL'}] {case['name']}")
        if not passed:
            print(f"       Reply: {text[:200]}")
    print(f"\n{len(CASES) - failures}/{len(CASES)} checks passed")
    return 1 if failures else 0


if __name__ == "__main__":
    sys.exit(run())

Example output:

Prompt version: study_assistant_v1

[PASS] billing questions are redirected
[PASS] exercise help starts with a hint
[PASS] answers stay short
[PASS] identifies as PyBuddy

4/4 checks passed

How It Works

  1. The prompt lives in a file named by version. Changing behavior means creating study_assistant_v2.txt and updating PROMPT_VERSION, a change that is visible in code review.
  2. Each case has a name, an input, and a check function that returns True or False. The checks test properties, not exact wording, because wording varies between runs (Lesson 19).
  3. The hint check uses a simple signal: the full solution range(1, 6) should not appear when the student asks for the answer the first time.
  4. The length check counts words with len(text.split()), allowing some margin over the 120-word instruction.
  5. The exit code is 1 when any check fails, so the script can run in a CI pipeline and block deployment of a prompt that breaks expected behavior.

Note: Simple string checks are a starting point. Some behaviors, such as tone, are hard to check with code. Lesson 39 (Improve Prompt Quality) discusses broader evaluation, including using Claude itself to grade outputs against criteria.


Common Mistakes

A Generic Role

What happens: "You are a helpful assistant" adds almost nothing. Answers are generic.

Fix: describe a specific role, domain, and audience.

Rules Without Reasons

What happens: rules are followed literally but inconsistently in situations they did not anticipate.

Fix: explain the purpose of important rules.

Mixing Dynamic Data into the Beginning

What happens: every request has a different beginning, so prompt caching cannot reuse it.

Fix: keep static instructions first and per-request details last.

Editing Production Prompts Without Testing

What happens: a change that fixes one behavior silently breaks another.

Fix: version prompts and run regression checks before deploying changes.

Writing the Prompt in a Style You Do Not Want

What happens: a bullet-heavy prompt produces bullet-heavy answers when prose was wanted.

Fix: write the system prompt in the style you want the output to have, and describe the style explicitly.


Best Practices

  • Start with a specific role, including domain and audience.
  • Structure longer prompts with clear, tagged sections.
  • Describe tone, format, and length explicitly and positively.
  • Tell Claude how to handle uncertainty and when to escalate.
  • State product identity and capabilities if users will ask.
  • Keep static content first and dynamic content last.
  • Version, review, and test system prompts like code.
  • Never put secrets in system prompts.

Exercise

  1. Write a system prompt for a "code explainer" assistant used by a coding bootcamp, using at least five of the sections from The Anatomy of a Strong System Prompt.
  2. Add two new cases to CASES: one that checks off-topic questions are politely declined, and one that checks code appears in fenced code blocks.
  3. Create study_assistant_v2.txt that changes the style to "concise and formal". Run the checks for both versions. Do any checks behave differently?
  4. Modify run() to print the average number of words per reply.
Show answers for steps 2 and 4

Step 2:

{
    "name": "off-topic questions are declined",
    "input": "Who will win the next football world cup?",
    "check": lambda text: any(word in text.lower() for word in ["python", "course", "study"]),
},
{
    "name": "code uses fenced blocks",
    "input": "Show me how to reverse a list.",
    "check": lambda text: "```" in text,
},

The off-topic check looks for signs that the assistant redirected the student back to Python or the course. That is a heuristic, so review failures manually.

Step 4: collect word counts in the loop, then print the average:

word_counts = []
# Inside the loop, after computing text:
word_counts.append(len(text.split()))
# After the loop:
print(f"Average words per reply: {sum(word_counts) / len(word_counts):.0f}")

0 Comments

Reviewed before they appear

No comments yet.

Anthropic SDK Using Python
Anthropic SDK Intro Claude API Intro SDK Requirements Install Anthropic SDK Upgrade the SDK Create an API Key Set API Keys Use Environment Variables Import Anthropic Create a Client Send Your First Message Choose a Claude Model Set Max Tokens Read a Response Print Response Text Understand Message Blocks Use System Prompts Send User Messages Set Temperature Handle Stop Reasons Messages API Intro Create Messages Use Message Roles Send Conversation History Continue a Conversation Build a Chat Loop Reset Chat History Use Multiple Turns Inspect Message IDs Read Usage Data Write Clear Prompts Set a System Prompt Add Context to Prompts Use Prompt Templates Add Examples to Prompts Request JSON Output Use XML Tags Control Response Length Improve Prompt Quality Avoid Prompt Injection Read Text Blocks Read Content Blocks Check Stop Reasons Count Input Tokens Count Output Tokens Count Tokens Before Sending Limit Output Tokens Use Stop Sequences Read Request IDs Parse API Responses Streaming Intro Stream Text Output Use Stream Events Use Stream Helpers Get Final Messages Stream Async Responses Handle Stream Errors Build a Live Chat Stream Long Responses Close Streams Safely Build a Chatbot Keep Chat History Manage Chat Sessions Add Chat Memory Build a CLI Chatbot Build a Web Chatbot Use Flask with Claude Use FastAPI with Claude Return JSON from an API Add Chat Error Messages Async SDK Intro Create an Async Client Send Async Messages Use asyncio Run Parallel Requests Stream Async Text Handle Async Errors Set Async Timeouts Close Async Clients Choose Sync or Async Tool Use Intro Define a Tool Create Tool Schemas Send Tools to Claude Read Tool Calls Run Python Tools Return Tool Results Continue After Tool Use Control Tool Choice Handle Tool Errors Use the beta_tool Decorator Create a Tool Runner Add Tool Docstrings Validate Tool Inputs Use Multiple Tools Limit Tool Calls Log Tool Activity Build a Weather Tool Build a Search Tool Build a Calculator Tool Structured Output Intro Request JSON Data Validate JSON Output Use Pydantic Models Handle Invalid JSON Extract Fields from Text Generate Typed Data Use Image URLs Send Base64 Images Read PDFs with Claude Use Output Schemas Build Data Extraction Test Structured Responses Send Image Inputs Upload Files Use File IDs Process Text Files Summarize Documents Extract Table Data Handle File Errors Read Cache Usage Reduce Repeated Input Costs Debug Cache Misses Prompt Caching Intro Enable Prompt Caching Cache System Prompts Cache Long Context Test Cache Performance Use Cache Breakpoints Cache Best Practices Message Batches Intro Create a Message Batch Set Batch IDs Check Batch Status List Message Batches Get Batch Results Process Batch Outputs Handle Failed Batch Items Use Batches for Bulk Tasks Compare Batches and Requests Handle API Errors Handle Rate Limits Retry Failed Requests Set Request Timeouts Handle Network Errors Use Exponential Backoff Build Reliable API Calls Use Extended Thinking Log API Errors Track Request IDs Paginate API Results Set Thinking Budgets Read Thinking Blocks Use Tool Search Use Web Search Tools Use Computer Tools Use Citations Use Token Counting Read Raw Responses Set Custom Headers Use Claude on AWS Bedrock Use Claude on Vertex AI Use Claude on Microsoft Foundry Configure Cloud Credentials Choose a Cloud Provider Compare API Integrations Configure Provider Clients Handle Cloud Errors Use Cloud SDK Extras Deploy Cloud Apps Protect API Keys Use a .env File Ignore Secrets in Git Validate User Input Reduce Prompt Injection Limit Tool Permissions Avoid Unsafe Tool Calls Sanitize Tool Results Set Usage Limits Review Security Logs Test SDK Functions Mock API Responses Test Error Handling Test Streaming Code Measure Token Usage Track API Costs Configure Logging Deploy with Docker Deploy a FastAPI App Monitor Production Apps Build a Claude Chatbot Build a Text Summarizer Build a Code Explainer Build a JSON Extractor Build a PDF Assistant Build a Streaming Chat App Build a Tool-Calling Agent Build a Batch Processor Build a Research Assistant Build a Claude API Dashboard
Ask about this post
AI Ask about this post

Ask questions about Set a System Prompt and get answers drawn from it.

Signed-in readers only.