Claude Code SDK - Python Reference Guide
This script is a complete reference for using the Claude Code SDK (claude-agent-sdk) in your Python scripts. It lets you call Claude programmatically — like an API — from any Python program.
"""
============================================================
Claude Code SDK - Python Reference Guide
============================================================
This script is a complete reference for using the Claude Code SDK
(claude-agent-sdk) in your Python scripts. It lets you call Claude
programmatically — like an API — from any Python program.
PREREQUISITES:
1. Install Claude Code CLI and authenticate:
npm install -g @anthropic-ai/claude-code
claude (follow the login prompts)
2. Install Python dependencies:
pip install claude-agent-sdk Pillow
3. That's it! The SDK uses your Claude Code CLI authentication.
No API key needed if you're on Claude Max plan.
USAGE:
python claude_sdk_reference.py # Run all examples
python claude_sdk_reference.py --example 1 # Run a specific example (1-5)
"""
import asyncio
import argparse
import base64
import io
import time
# ============================================================
# IMPORTS — Everything the SDK provides
# ============================================================
from claude_agent_sdk import (
query, # The main function — sends prompts and streams responses
ClaudeAgentOptions, # Configuration object for controlling Claude's behavior
AssistantMessage, # Streamed response chunks from Claude
ResultMessage, # Final message with usage stats and cost
TextBlock, # A block of text in Claude's response
)
# Also available but not used in every example:
# from claude_agent_sdk import UserMessage # For tracking user messages in the stream
# ============================================================
# EXAMPLE 1: Simple Text Query
# ============================================================
# The most basic usage — send a prompt, get a response.
# This is your "Hello World" for the SDK.
async def example_simple_query():
"""Send a simple text prompt and print the response."""
print("\n" + "=" * 60)
print(" EXAMPLE 1: Simple Text Query")
print("=" * 60)
response_text = ""
# query() is an async generator — you iterate over messages as they stream in.
# At minimum, you just need a `prompt` string.
async for message in query(prompt="What is Python? Reply in exactly one sentence."):
# AssistantMessage = Claude's response (may arrive in multiple chunks)
if isinstance(message, AssistantMessage):
for block in message.content:
if isinstance(block, TextBlock):
response_text += block.text
# ResultMessage = Final message after Claude is done (contains usage stats)
elif isinstance(message, ResultMessage):
pass # We'll cover usage tracking in Example 5
print(f"\n Response: {response_text}")
return response_text
# ============================================================
# EXAMPLE 2: Query with Options
# ============================================================
# ClaudeAgentOptions lets you control Claude's behavior:
# - max_turns: How many back-and-forth turns Claude can take (1 = single response)
# - permission_mode: What Claude is allowed to do
# - allowed_tools: Which tools Claude can use (Read, Write, Bash, etc.)
# - cwd: Working directory for file operations
async def example_with_options():
"""Demonstrate the most useful ClaudeAgentOptions settings."""
print("\n" + "=" * 60)
print(" EXAMPLE 2: Query with Options")
print("=" * 60)
# --- 2a: Limit Claude to a single turn (no tool use, just answer) ---
print("\n 2a) Single-turn query (max_turns=1):")
options = ClaudeAgentOptions(
max_turns=1, # Claude responds once, no follow-up tool calls
)
response = ""
async for message in query(
prompt="Write a Python one-liner that reverses a string. Return ONLY the code.",
options=options,
):
if isinstance(message, AssistantMessage):
for block in message.content:
if isinstance(block, TextBlock):
response += block.text
print(f" Response: {response}")
# --- 2b: Allow Claude to create files and run code ---
print("\n 2b) With file and shell permissions:")
import os
import tempfile
work_dir = tempfile.mkdtemp(prefix="claude_sdk_demo_")
options = ClaudeAgentOptions(
cwd=work_dir, # Set working directory
permission_mode="acceptEdits", # Allow file edits without asking
allowed_tools=["Read", "Write", "Bash"], # Only these tools are available
)
response = ""
async for message in query(
prompt=(
'Create a file called "hello.py" that prints "Hello from Claude SDK!" '
"and then run it."
),
options=options,
):
if isinstance(message, AssistantMessage):
for block in message.content:
if isinstance(block, TextBlock):
response += block.text
print(f" Response: {response[:200]}...")
# Verify the file was created
created_file = os.path.join(work_dir, "hello.py")
if os.path.exists(created_file):
with open(created_file) as f:
print(f" Created file contents: {f.read().strip()}")
return response
# ============================================================
# EXAMPLE 3: Extended Thinking
# ============================================================
# Extended thinking lets Claude "think step by step" before answering.
# Great for complex reasoning, math, code generation, and analysis.
#
# Key options:
# - thinking.budget_tokens: How many tokens Claude can use for thinking
# - effort: "low", "medium", or "high" — controls how hard Claude tries
async def example_extended_thinking():
"""Use extended thinking for complex reasoning tasks."""
print("\n" + "=" * 60)
print(" EXAMPLE 3: Extended Thinking")
print("=" * 60)
options = ClaudeAgentOptions(
# Enable extended thinking with a token budget
thinking={"type": "enabled", "budget_tokens": 5000},
effort="high", # Try harder (useful for complex tasks)
max_turns=1,
)
response_text = ""
thinking_used = False
async for message in query(
prompt="What are the first 10 prime numbers? Show your reasoning step by step.",
options=options,
):
if isinstance(message, AssistantMessage):
for block in message.content:
# Check if this is a thinking block (Claude's internal reasoning)
block_type = getattr(block, "type", None)
if block_type == "thinking":
thinking_text = getattr(block, "thinking", "")
thinking_used = True
print(f" [Thinking] {len(thinking_text):,} characters of reasoning")
# Regular text response
elif isinstance(block, TextBlock):
response_text += block.text
print(f"\n Thinking used: {thinking_used}")
print(f" Response: {response_text[:300]}...")
return response_text
# ============================================================
# EXAMPLE 4: Sending Images
# ============================================================
# To send images, you need an async generator that yields a
# properly formatted user message with content blocks.
#
# Content blocks can include:
# - {"type": "text", "text": "..."} — text content
# - {"type": "image", "source": {...}} — base64-encoded image
#
# This pattern is how you send any multi-part content to Claude.
async def create_image_message(content_blocks: list):
"""
Async generator that yields a properly formatted user message.
This is the REQUIRED pattern for sending images or multi-part
content to the SDK. You pass this generator to query() instead
of a plain string prompt.
"""
yield {
"type": "user",
"message": {
"role": "user",
"content": content_blocks,
},
}
async def example_send_image():
"""Send an image to Claude for analysis."""
print("\n" + "=" * 60)
print(" EXAMPLE 4: Sending Images")
print("=" * 60)
# --- Step 1: Prepare the image as base64 ---
# You can load from a file or create one programmatically.
# Here we create a simple test image with Pillow.
try:
from PIL import Image
except ImportError:
print(" Pillow not installed. Run: pip install Pillow")
return ""
# Create a simple gradient image as a demo
img = Image.new("RGB", (200, 200), color=(70, 130, 180)) # Steel blue
buffer = io.BytesIO()
img.save(buffer, format="JPEG", quality=70)
# Encode to base64 string
image_data = base64.standard_b64encode(buffer.getvalue()).decode("utf-8")
print(f" Image size: {len(image_data) / 1024:.1f} KB (base64)")
# --- Step 2: Build content blocks (text + image) ---
content_blocks = [
# Text block — your question or instruction
{"type": "text", "text": "Describe this image in one sentence. What color do you see?"},
# Image block — the base64-encoded image
{
"type": "image",
"source": {
"type": "base64",
"media_type": "image/jpeg", # or "image/png"
"data": image_data,
},
},
]
# --- Step 3: Send using the async generator pattern ---
options = ClaudeAgentOptions(max_turns=1)
response_text = ""
# NOTE: We pass the async generator, NOT a string prompt
async for message in query(
prompt=create_image_message(content_blocks),
options=options,
):
if isinstance(message, AssistantMessage):
for block in message.content:
if isinstance(block, TextBlock):
response_text += block.text
print(f" Response: {response_text}")
return response_text
# ============================================================
# EXAMPLE 5: Usage & Cost Tracking
# ============================================================
# The ResultMessage contains detailed usage information:
# - usage: input/output tokens, cache stats
# - total_cost_usd: total cost in USD
# - model_usage: per-model breakdown (input, output, cost)
#
# On Claude Max plan, the cost reflects what it *would* cost
# on API billing — it's included in your subscription.
async def example_usage_tracking():
"""Track token usage and costs from Claude's response."""
print("\n" + "=" * 60)
print(" EXAMPLE 5: Usage & Cost Tracking")
print("=" * 60)
response_text = ""
usage_data = None
total_cost = None
model_usage = None
options = ClaudeAgentOptions(max_turns=1)
async for message in query(
prompt="Explain what an API is in 2 sentences.",
options=options,
):
# --- Collect response text ---
if isinstance(message, AssistantMessage):
for block in message.content:
if isinstance(block, TextBlock):
response_text += block.text
# Each AssistantMessage also has step-level usage
if hasattr(message, "usage") and message.usage:
print(f" [step usage] {message.usage}")
# --- Extract final usage from ResultMessage ---
elif isinstance(message, ResultMessage):
# Cumulative token usage
if hasattr(message, "usage") and message.usage:
usage_data = message.usage
# Total cost in USD
if hasattr(message, "total_cost_usd"):
total_cost = message.total_cost_usd
# Per-model breakdown (useful when Claude uses sub-agents)
if hasattr(message, "model_usage"):
model_usage = message.model_usage
# --- Display results ---
print(f"\n Response: {response_text}\n")
print(" ┌─────────────────────────────────────")
print(" │ USAGE REPORT")
print(" ├─────────────────────────────────────")
if usage_data:
print(f" │ Input tokens: {usage_data.get('input_tokens', 'N/A')}")
print(f" │ Output tokens: {usage_data.get('output_tokens', 'N/A')}")
print(f" │ Cache read: {usage_data.get('cache_read_input_tokens', 'N/A')}")
print(f" │ Cache create: {usage_data.get('cache_creation_input_tokens', 'N/A')}")
else:
print(" │ (no usage data returned)")
if total_cost is not None:
print(f" │ Total cost: ${total_cost:.6f}")
if model_usage:
print(" │")
print(" │ Per-model breakdown:")
for model_name, mu in model_usage.items():
cost = mu.get("costUSD", mu.get("cost_usd", "N/A"))
inp = mu.get("inputTokens", mu.get("input_tokens", "N/A"))
out = mu.get("outputTokens", mu.get("output_tokens", "N/A"))
print(f" │ {model_name}: in={inp}, out={out}, cost=${cost}")
print(" └─────────────────────────────────────")
if total_cost is not None:
print("\n Note: On Claude Max plan, this cost is included in your subscription.")
return usage_data
# ============================================================
# QUICK REFERENCE — ClaudeAgentOptions cheat sheet
# ============================================================
#
# ClaudeAgentOptions(
# max_turns=1, # Limit back-and-forth turns
# cwd="/path/to/dir", # Working directory for file ops
# permission_mode="acceptEdits", # "default" | "acceptEdits" | "bypassPermissions"
# allowed_tools=["Read", "Write", "Bash"], # Restrict available tools
# thinking={ # Extended thinking
# "type": "enabled",
# "budget_tokens": 15000, # Token budget for thinking
# },
# effort="high", # "low" | "medium" | "high"
# max_buffer_size=50_000_000, # Max output buffer (bytes) for large responses
# )
#
# query() accepts:
# prompt="string" # Simple text prompt
# prompt=async_generator # For images/multi-part content
# options=ClaudeAgentOptions() # Configuration
#
# Message types you'll receive:
# AssistantMessage — Claude's response (text, thinking blocks)
# ResultMessage — Final stats (usage, cost, model_usage)
# UserMessage — Echo of user messages (usually ignored)
#
# ============================================================
# MAIN — Run examples
# ============================================================
async def main():
parser = argparse.ArgumentParser(description="Claude Code SDK Reference Examples")
parser.add_argument(
"--example", type=int, choices=[1, 2, 3, 4, 5],
help="Run a specific example (1-5). Omit to run all.",
)
args = parser.parse_args()
examples = {
1: ("Simple Text Query", example_simple_query),
2: ("Query with Options", example_with_options),
3: ("Extended Thinking", example_extended_thinking),
4: ("Sending Images", example_send_image),
5: ("Usage & Cost Tracking", example_usage_tracking),
}
if args.example:
# Run single example
label, func = examples[args.example]
print(f"\nRunning Example {args.example}: {label}")
await func()
else:
# Run all examples
print("\n" + "=" * 60)
print(" CLAUDE CODE SDK — Running All Examples")
print("=" * 60)
for num, (label, func) in examples.items():
try:
await func()
except Exception as e:
print(f"\n Example {num} ({label}) failed: {type(e).__name__}: {e}")
import traceback
traceback.print_exc()
print("\n" + "=" * 60)
print(" All done! Happy coding.")
print("=" * 60 + "\n")
if __name__ == "__main__":
asyncio.run(main())