Prompt and Context Engineering Basics
9 examples to get you started with Prompt and Context Engineering - 6 basic and 3 intermediate.
Prerequisites
- Install the official SDK:
pip install anthropic. - Set your API key as an environment variable:
export ANTHROPIC_API_KEY=sk-ant-.... - All examples use
client.messages.create(...)from theanthropicPython package.
Basic Examples
1. The Naive, Bloated Prompt
The starting point most teams accidentally ship: pasting an entire file when only a few lines matter.
import anthropic
client = anthropic.Anthropic()
with open("user_service.py") as f:
entire_file = f.read() # could be 2,000+ lines
response = client.messages.create(
model="claude-sonnet-5",
max_tokens=300,
messages=[{
"role": "user",
"content": f"Does this function validate the email format?\n\n{entire_file}",
}],
)- The whole file is sent even though the question is about one function.
- Every extra line in
entire_fileis billed as an input token on every call. - This pattern is the default failure mode, not an edge case, because it requires zero extra thought to write.
- Nothing here is wrong syntactically, which is why it's easy to miss in review.
2. Trimming to the Relevant Snippet
The same question, answered by sending only the function in question.
import anthropic
client = anthropic.Anthropic()
relevant_function = '''
def validate_email(address: str) -> bool:
return "@" in address and "." in address.split("@")[-1]
'''
response = client.messages.create(
model="claude-sonnet-5",
max_tokens=300,
messages=[{
"role": "user",
"content": f"Does this function validate the email format?\n\n{relevant_function}",
}],
)- The prompt now contains exactly the code the question is about, nothing else.
- Fewer input tokens means a lower cost for this exact same question.
- Extracting the relevant snippet is the one manual step this approach requires.
- This is minimal-context prompting: send only what the task needs.
3. Measuring the Token Difference
Before trusting that trimming actually helped, measure it.
import anthropic
client = anthropic.Anthropic()
def count_tokens(text: str, model: str = "claude-sonnet-5") -> int:
result = client.messages.count_tokens(
model=model,
messages=[{"role": "user", "content": text}],
)
return result.input_tokens
full_file_tokens = count_tokens(open("user_service.py").read())
snippet_tokens = count_tokens(relevant_function)
print(f"Full file: {full_file_tokens} tokens")
print(f"Snippet: {snippet_tokens} tokens")count_tokensreports input tokens without generating a completion, so measuring is nearly free.- Comparing before and after turns "this feels smaller" into an actual number you can put in a cost report.
- Run this once per prompt template during development, not on every production call.
- If the snippet isn't meaningfully smaller than the full file, the trimming step isn't doing its job.
4. A Reusable Prompt Template
Boilerplate instructions repeated in every call waste tokens just as much as an oversized document.
SYSTEM_PROMPT = "You are a code reviewer. Answer in one short paragraph."
def review(snippet: str, question: str) -> str:
response = client.messages.create(
model="claude-sonnet-5",
max_tokens=200,
system=SYSTEM_PROMPT,
messages=[{"role": "user", "content": f"{question}\n\n{snippet}"}],
)
return response.content[0].text- Fixing the system prompt in one place keeps every call's instructions short and consistent.
- The variable part of the prompt (
snippet,question) is the only thing that changes per call. - This separation also makes it obvious, at a glance, what's fixed cost versus per-call cost.
- A tidy template is a prerequisite for the summarization and tiering techniques covered later in this section.
5. Dropping Stale Conversation History
In a multi-turn chat, old turns that no longer matter still cost tokens if you keep resending them.
def trimmed_history(messages: list[dict], keep_last: int = 4) -> list[dict]:
# Keep only the most recent turns; drop everything older.
return messages[-keep_last:]
full_history = [
{"role": "user", "content": "What does this repo do?"},
{"role": "assistant", "content": "It's a billing service..."},
{"role": "user", "content": "Add a retry to the webhook handler."},
{"role": "assistant", "content": "Here's the updated function..."},
{"role": "user", "content": "Now add a unit test for it."},
]
response = client.messages.create(
model="claude-sonnet-5",
max_tokens=400,
messages=trimmed_history(full_history, keep_last=4),
)- Every prior turn in
messagesis resent as input tokens on the next call, so history grows costlier the longer a conversation runs. trimmed_historyis a blunt tool: it keeps recency, not relevance, so use it when older turns genuinely stop mattering.- For conversations where an early turn stays relevant throughout, trimming by recency alone can drop something you still need.
- This is the same minimal-context idea applied to conversation state instead of a single document.
6. Excluding Irrelevant Fields From Structured Data
The same discipline applies to JSON or database rows, not just source files.
raw_record = {
"id": "usr_9f2",
"email": "a@example.com",
"created_at": "2024-01-02T00:00:00Z",
"internal_flags": {"beta": True, "region_shard": "us-east-2"},
"last_login_ip": "10.0.4.12",
"support_notes": "..." * 500, # long free-text field
}
def trim_for_prompt(record: dict) -> dict:
return {k: record[k] for k in ("id", "email", "created_at")}
response = client.messages.create(
model="claude-sonnet-5",
max_tokens=100,
messages=[{
"role": "user",
"content": f"Summarize this account: {trim_for_prompt(raw_record)}",
}],
)- Structured data has the same problem as source files: it's easy to pass the whole object instead of the fields the task needs.
trim_for_promptis an explicit allowlist, which is safer than an ad hoc denylist that silently keeps new fields added later.- Long free-text fields like
support_notesare exactly the kind of thing that quietly inflates token counts. - This pattern generalizes to any API response or database row a pipeline feeds into a prompt.
Intermediate Examples
7. A Minimal-Context Snippet Selector
Instead of manually picking a snippet, rank candidates by relevance and keep only the top few.
import anthropic
client = anthropic.Anthropic()
def select_relevant_snippets(question: str, candidates: list[str], keep: int = 2) -> list[str]:
# A cheap relevance pass: ask a fast model to score each candidate.
scored = []
for snippet in candidates:
response = client.messages.create(
model="claude-haiku-4-5",
max_tokens=5,
messages=[{
"role": "user",
"content": f"Question: {question}\nSnippet:\n{snippet}\n\nRelevance 0-10, number only:",
}],
)
score = int(response.content[0].text.strip() or 0)
scored.append((score, snippet))
scored.sort(reverse=True, key=lambda pair: pair[0])
return [snippet for _, snippet in scored[:keep]]
top_snippets = select_relevant_snippets(
"How is a failed payment retried?",
candidates=[billing_module_src, auth_module_src, retry_queue_src],
keep=2,
)- A cheap model like Haiku 4.5 scores relevance far more cheaply than paying to send every candidate to the expensive model.
- Only the top-scoring snippets go into the real prompt sent to the stronger model doing the actual task.
- This is model tiering applied to the selection step itself, not just the final answer.
- For a codebase, a dependency graph is a faster and more precise version of this same idea; see the dedicated page on that technique.
8. Combining Trimming With Token Budgeting
A context window budget caps how much can be sent, and trimming is how you stay under it.
MAX_CONTEXT_TOKENS = 4000
def build_prompt_within_budget(question: str, snippets: list[str]) -> str:
prompt = question
used = count_tokens(prompt)
for snippet in snippets:
snippet_tokens = count_tokens(snippet)
if used + snippet_tokens > MAX_CONTEXT_TOKENS:
break # stop adding snippets once the budget is hit
prompt += f"\n\n{snippet}"
used += snippet_tokens
return prompt
final_prompt = build_prompt_within_budget(
"Summarize the recent changes to the retry logic.",
snippets=[retry_queue_diff, billing_module_diff, changelog_excerpt],
)- Setting an explicit
MAX_CONTEXT_TOKENSturns "keep the prompt small" into a rule the code enforces, not a habit engineers have to remember. - Snippets are added in priority order, so the most relevant material survives if the budget forces a cutoff.
- This same budgeting idea, applied to a long-running agent instead of a single call, is what a context window budget ADR documents formally.
- A budget that's too tight silently drops relevant material, so pick it based on measured needs, not a round number.
9. Measuring Savings Across a Full Pipeline
Put the earlier pieces together and report the actual cost impact of trimming.
def compare_strategies(question: str, full_document: str, trimmed_snippet: str) -> dict:
return {
"full_document_tokens": count_tokens(f"{question}\n\n{full_document}"),
"trimmed_tokens": count_tokens(f"{question}\n\n{trimmed_snippet}"),
}
result = compare_strategies(
"Does this file handle retries correctly?",
full_document=open("billing_service.py").read(),
trimmed_snippet=relevant_function,
)
savings_pct = 100 * (1 - result["trimmed_tokens"] / result["full_document_tokens"])
print(f"Token reduction: {savings_pct:.0f}%")- This is the same
count_tokenscall from example 3, applied at the pipeline level instead of one prompt at a time. - Reporting a percentage reduction, rather than a raw token count, makes the savings meaningful to non-engineering stakeholders.
- Run this comparison whenever you change how context is assembled, so a regression in prompt size gets caught the same way a performance regression would.
- Token savings compound across every call a workload makes, so a modest per-call reduction can add up to a large monthly difference.
Stack versions: Written against the Claude model lineup current as of ~June 2026 - Claude Fable 5, Claude Opus 4.8, Claude Sonnet 5 (the default), and Claude Haiku 4.5 - and the official
anthropicPython SDK (latest 0.x release). Model names, pricing, and SDK versions move quickly - verify current specifics at platform.claude.com/docs before relying on them.