Claude Agent SDK - Python vs TypeScript
Overview
Claude Agent SDK is a library that enables programmatic building of AI agents with capabilities like codebase understanding, file editing, command execution, and complex multi-step workflows. It’s available in both Python and TypeScript.
Feature Comparison
| Feature | Python | TypeScript |
|---|---|---|
| Basic Query | query() function | query() function |
| Session Management | ClaudeSDKClient class | Built into query() |
| Custom Tools | @tool decorator | tool() function + Zod schema |
| MCP Servers | create_sdk_mcp_server() | createSdkMcpServer() |
| Hooks Support | Partial (no SessionStart/End/Notification) | Full support |
| Interrupt Operations | Only with ClaudeSDKClient | Supported |
| Streaming Input | Supported | Supported |
| V2 Interface | None | Available (preview) - simplified send()/receive() |
| Type Safety | Via dataclass | Via Zod + TypeScript types |
| Runtime Requirements | Python 3.10+ | Node.js 18+ |
Installation
Python
pip install claude-agent-sdkTypeScript
npm install @anthropic-ai/claude-agent-sdkAPI Design Differences
Python: Two Approaches
Python SDK provides two ways to interact with Claude:
1. query() - For one-off tasks
from claude_agent_sdk import query, ClaudeAgentOptionsimport asyncio
async def main(): options = ClaudeAgentOptions( system_prompt="You are an expert Python developer", permission_mode='acceptEdits', cwd="/home/user/project" )
async for message in query( prompt="Create a Python web server", options=options ): print(message)
asyncio.run(main())2. ClaudeSDKClient - For continuous conversations
from claude_agent_sdk import ClaudeSDKClient, AssistantMessage, TextBlock
async def main(): async with ClaudeSDKClient() as client: # First question await client.query("What's the capital of France?") async for message in client.receive_response(): if isinstance(message, AssistantMessage): for block in message.content: if isinstance(block, TextBlock): print(f"Claude: {block.text}")
# Follow-up - Claude remembers context await client.query("What's the population of that city?") async for message in client.receive_response(): # Process response... pass
asyncio.run(main())TypeScript: Unified Query Interface
import { query } from "@anthropic-ai/claude-agent-sdk";
const result = query({ prompt: "Create a Python web server", options: { systemPrompt: "You are an expert Python developer", permissionMode: 'acceptEdits', cwd: "/home/user/project" }});
for await (const message of result) { console.log(message);}Custom Tools
Python
from claude_agent_sdk import tool, create_sdk_mcp_serverfrom typing import Any
@tool("calculate", "Perform mathematical calculations", {"expression": str})async def calculate(args: dict[str, Any]) -> dict[str, Any]: try: result = eval(args["expression"], {"__builtins__": {}}) return { "content": [{ "type": "text", "text": f"Result: {result}" }] } except Exception as e: return { "content": [{"type": "text", "text": f"Error: {str(e)}"}], "is_error": True }
calculator = create_sdk_mcp_server( name="calculator", version="1.0.0", tools=[calculate])TypeScript
import { tool, createSdkMcpServer } from "@anthropic-ai/claude-agent-sdk";import { z } from "zod";
const calculate = tool( "calculate", "Perform mathematical calculations", { expression: z.string() }, async (args) => { const result = eval(args.expression); return { content: [{ type: "text", text: `Result: ${result}` }] }; });
const calculator = createSdkMcpServer({ name: "calculator", version: "1.0.0", tools: [calculate]});Hooks Support
Python Limitations
Python SDK does not support these hooks due to setup limitations:
SessionStartSessionEndNotification
TypeScript Full Support
All hook events are available:
const options = { hooks: { PreToolUse: [{ matcher: 'Bash', hooks: [async (input, toolUseId, { signal }) => { console.log(`About to use: ${input.tool_name}`); return {}; }] }], SessionStart: [{ hooks: [async (input) => { console.log('Session started'); return {}; }] }] }};More Examples
Streaming Input
Python
import asynciofrom claude_agent_sdk import ClaudeSDKClient
async def message_stream(): """Generate messages dynamically.""" yield {"type": "text", "text": "Analyze the following data:"} await asyncio.sleep(0.5) yield {"type": "text", "text": "Temperature: 25C, Humidity: 60%"} await asyncio.sleep(0.5) yield {"type": "text", "text": "What patterns do you see?"}
async def main(): async with ClaudeSDKClient() as client: await client.query(message_stream()) async for message in client.receive_response(): print(message)
asyncio.run(main())TypeScript
import { query } from "@anthropic-ai/claude-agent-sdk";
async function* messageStream() { yield { type: "user", message: { role: "user", content: "Analyze this data:" } }; await new Promise(r => setTimeout(r, 500)); yield { type: "user", message: { role: "user", content: "Temperature: 25C" } };}
const result = query({ prompt: messageStream(), options: { permissionMode: "acceptEdits" }});
for await (const message of result) { console.log(message);}Interrupt Operations
Python
import asynciofrom claude_agent_sdk import ClaudeSDKClient, ClaudeAgentOptions
async def interruptible_task(): options = ClaudeAgentOptions( allowed_tools=["Bash"], permission_mode="acceptEdits" )
async with ClaudeSDKClient(options=options) as client: # Start a long-running task await client.query("Count from 1 to 100 slowly")
# Let it run for a bit await asyncio.sleep(2)
# Interrupt the task await client.interrupt() print("Task interrupted!")
# Send a new command await client.query("Just say hello instead") async for message in client.receive_response(): print(message)
asyncio.run(interruptible_task())TypeScript
import { query } from "@anthropic-ai/claude-agent-sdk";
const abortController = new AbortController();
const result = query({ prompt: "Count from 1 to 100 slowly", options: { abortController, allowedTools: ["Bash"], permissionMode: "acceptEdits" }});
// Interrupt after 2 secondssetTimeout(() => { abortController.abort(); console.log("Task interrupted!");}, 2000);
for await (const message of result) { console.log(message);}Custom Permission Handler
Python
from claude_agent_sdk import ClaudeSDKClient, ClaudeAgentOptionsfrom claude_agent_sdk.types import PermissionResultAllow, PermissionResultDeny
async def custom_permission_handler( tool_name: str, input_data: dict, context: dict) -> PermissionResultAllow | PermissionResultDeny: """Custom logic for tool permissions."""
# Block writes to system directories if tool_name == "Write": file_path = input_data.get("file_path", "") if file_path.startswith("/system/") or file_path.startswith("/etc/"): return PermissionResultDeny( message="System directory write not allowed", interrupt=True )
# Redirect sensitive file operations to sandbox if tool_name in ["Write", "Edit"] and "config" in input_data.get("file_path", ""): safe_path = f"./sandbox/{input_data['file_path']}" return PermissionResultAllow( updated_input={**input_data, "file_path": safe_path} )
return PermissionResultAllow(updated_input=input_data)
async def main(): options = ClaudeAgentOptions( can_use_tool=custom_permission_handler, allowed_tools=["Read", "Write", "Edit"] )
async with ClaudeSDKClient(options=options) as client: await client.query("Create a config file") async for message in client.receive_response(): print(message)
asyncio.run(main())TypeScript
import { query } from "@anthropic-ai/claude-agent-sdk";
const result = query({ prompt: "Create a config file", options: { allowedTools: ["Read", "Write", "Edit"], canUseTool: async (toolName, input, { signal }) => { // Block writes to system directories if (toolName === "Write") { const filePath = input.file_path as string; if (filePath.startsWith("/system/") || filePath.startsWith("/etc/")) { return { behavior: "deny", message: "System directory write not allowed", interrupt: true }; } }
// Redirect config files to sandbox if (["Write", "Edit"].includes(toolName) && (input.file_path as string).includes("config")) { return { behavior: "allow", updatedInput: { ...input, file_path: `./sandbox/${input.file_path}` } }; }
return { behavior: "allow", updatedInput: input }; } }});
for await (const message of result) { console.log(message);}Hooks for Security and Logging
Python
from claude_agent_sdk import query, ClaudeAgentOptions, HookMatcher, HookContextfrom typing import Any
async def validate_bash_command( input_data: dict[str, Any], tool_use_id: str | None, context: HookContext) -> dict[str, Any]: """Block dangerous bash commands.""" if input_data['tool_name'] == 'Bash': command = input_data['tool_input'].get('command', '') dangerous_patterns = ['rm -rf', 'mkfs', 'dd if=', ':(){:|:&};:']
for pattern in dangerous_patterns: if pattern in command: return { 'hookSpecificOutput': { 'hookEventName': 'PreToolUse', 'permissionDecision': 'deny', 'permissionDecisionReason': f'Dangerous command blocked: {pattern}' } } return {}
async def log_tool_use( input_data: dict[str, Any], tool_use_id: str | None, context: HookContext) -> dict[str, Any]: """Log all tool usage for auditing.""" print(f"[AUDIT] Tool: {input_data.get('tool_name')}, Input: {input_data.get('tool_input')}") return {}
options = ClaudeAgentOptions( hooks={ 'PreToolUse': [ HookMatcher(matcher='Bash', hooks=[validate_bash_command]), HookMatcher(hooks=[log_tool_use]) ], 'PostToolUse': [ HookMatcher(hooks=[log_tool_use]) ] }, allowed_tools=["Bash", "Read", "Write"])
async def main(): async for message in query(prompt="List files and show disk usage", options=options): print(message)
asyncio.run(main())TypeScript
import { query } from "@anthropic-ai/claude-agent-sdk";
const dangerousPatterns = ['rm -rf', 'mkfs', 'dd if=', ':(){:|:&};:'];
const result = query({ prompt: "List files and show disk usage", options: { allowedTools: ["Bash", "Read", "Write"], hooks: { PreToolUse: [{ matcher: 'Bash', hooks: [async (input, toolUseId, { signal }) => { const command = input.tool_input?.command || '';
for (const pattern of dangerousPatterns) { if (command.includes(pattern)) { return { hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: `Dangerous command blocked: ${pattern}` } }; } } return {}; }] }, { hooks: [async (input) => { console.log(`[AUDIT] Tool: ${input.tool_name}, Input:`, input.tool_input); return {}; }] }], PostToolUse: [{ hooks: [async (input) => { console.log(`[AUDIT] Completed: ${input.tool_name}`); return {}; }] }] } }});
for await (const message of result) { console.log(message);}Using MCP Servers with Multiple Tools
Python
from claude_agent_sdk import ( ClaudeSDKClient, ClaudeAgentOptions, tool, create_sdk_mcp_server)from typing import Anyimport json
@tool("get_weather", "Get weather for a city", {"city": str})async def get_weather(args: dict[str, Any]) -> dict[str, Any]: # Simulated weather data weather_data = { "Tokyo": {"temp": 22, "condition": "Sunny"}, "London": {"temp": 15, "condition": "Cloudy"}, "New York": {"temp": 18, "condition": "Partly Cloudy"} } city = args["city"] data = weather_data.get(city, {"temp": 20, "condition": "Unknown"}) return { "content": [{ "type": "text", "text": f"Weather in {city}: {data['temp']}C, {data['condition']}" }] }
@tool("convert_currency", "Convert between currencies", {"amount": float, "from_currency": str, "to_currency": str})async def convert_currency(args: dict[str, Any]) -> dict[str, Any]: # Simulated exchange rates rates = {"USD": 1.0, "EUR": 0.85, "GBP": 0.73, "JPY": 110.0} amount = args["amount"] from_curr = args["from_currency"] to_curr = args["to_currency"]
usd_amount = amount / rates.get(from_curr, 1.0) result = usd_amount * rates.get(to_curr, 1.0)
return { "content": [{ "type": "text", "text": f"{amount} {from_curr} = {result:.2f} {to_curr}" }] }
@tool("search_database", "Search a database", {"query": str, "limit": int})async def search_database(args: dict[str, Any]) -> dict[str, Any]: # Simulated database search results = [ {"id": 1, "name": "Item A", "price": 100}, {"id": 2, "name": "Item B", "price": 200}, ][:args.get("limit", 10)]
return { "content": [{ "type": "text", "text": json.dumps(results, indent=2) }] }
# Create MCP server with all toolsutilities_server = create_sdk_mcp_server( name="utilities", version="1.0.0", tools=[get_weather, convert_currency, search_database])
async def main(): options = ClaudeAgentOptions( mcp_servers={"utils": utilities_server}, allowed_tools=[ "mcp__utils__get_weather", "mcp__utils__convert_currency", "mcp__utils__search_database" ] )
async with ClaudeSDKClient(options=options) as client: await client.query( "What's the weather in Tokyo? Also convert 100 USD to JPY." ) async for message in client.receive_response(): print(message)
asyncio.run(main())TypeScript
import { query, tool, createSdkMcpServer } from "@anthropic-ai/claude-agent-sdk";import { z } from "zod";
const getWeather = tool( "get_weather", "Get weather for a city", { city: z.string() }, async (args) => { const weatherData: Record<string, { temp: number; condition: string }> = { "Tokyo": { temp: 22, condition: "Sunny" }, "London": { temp: 15, condition: "Cloudy" }, "New York": { temp: 18, condition: "Partly Cloudy" } }; const data = weatherData[args.city] || { temp: 20, condition: "Unknown" }; return { content: [{ type: "text", text: `Weather in ${args.city}: ${data.temp}C, ${data.condition}` }] }; });
const convertCurrency = tool( "convert_currency", "Convert between currencies", { amount: z.number(), from_currency: z.string(), to_currency: z.string() }, async (args) => { const rates: Record<string, number> = { USD: 1.0, EUR: 0.85, GBP: 0.73, JPY: 110.0 }; const usdAmount = args.amount / (rates[args.from_currency] || 1.0); const result = usdAmount * (rates[args.to_currency] || 1.0); return { content: [{ type: "text", text: `${args.amount} ${args.from_currency} = ${result.toFixed(2)} ${args.to_currency}` }] }; });
const utilitiesServer = createSdkMcpServer({ name: "utilities", version: "1.0.0", tools: [getWeather, convertCurrency]});
const result = query({ prompt: "What's the weather in Tokyo? Also convert 100 USD to JPY.", options: { mcpServers: { utils: utilitiesServer }, allowedTools: [ "mcp__utils__get_weather", "mcp__utils__convert_currency" ] }});
for await (const message of result) { console.log(message);}Error Handling
Python
from claude_agent_sdk import ( query, CLINotFoundError, ProcessError, CLIJSONDecodeError, CLIConnectionError)import asyncio
async def main(): try: async for message in query(prompt="Hello, Claude!"): print(message) except CLINotFoundError as e: print(f"CLI not found: {e}") print("Please install: npm install -g @anthropic-ai/claude-code") except CLIConnectionError as e: print(f"Connection failed: {e}") except ProcessError as e: print(f"Process error (exit code {e.exit_code}): {e}") if e.stderr: print(f"stderr: {e.stderr}") except CLIJSONDecodeError as e: print(f"JSON parse error on line: {e.line}") print(f"Original error: {e.original_error}")
asyncio.run(main())TypeScript
import { query, AbortError } from "@anthropic-ai/claude-agent-sdk";
async function main() { try { const result = query({ prompt: "Hello, Claude!", options: {} });
for await (const message of result) { if (message.type === 'result') { if (message.is_error) { console.error("Agent error:", message.subtype); if ('errors' in message) { console.error("Details:", message.errors); } } else { console.log("Success:", message.result); console.log("Cost:", message.total_cost_usd, "USD"); } } } } catch (error) { if (error instanceof AbortError) { console.log("Operation was aborted"); } else if (error instanceof Error) { console.error("Error:", error.message); } }}
main();Sandbox Mode
Python
from claude_agent_sdk import query, ClaudeAgentOptionsimport asyncio
async def main(): options = ClaudeAgentOptions( sandbox={ "enabled": True, "autoAllowBashIfSandboxed": True, "network": { "allowLocalBinding": True # Allow dev servers }, "excludedCommands": ["docker"], # Docker bypasses sandbox "ignoreViolations": { "file": ["/tmp/*"], "network": ["localhost:*"] } }, allowed_tools=["Bash", "Write", "Read"], permission_mode="acceptEdits" )
async for message in query( prompt="Create a simple web server and test it", options=options ): print(message)
asyncio.run(main())TypeScript
import { query } from "@anthropic-ai/claude-agent-sdk";
const result = query({ prompt: "Create a simple web server and test it", options: { sandbox: { enabled: true, autoAllowBashIfSandboxed: true, network: { allowLocalBinding: true }, excludedCommands: ["docker"], ignoreViolations: { file: ["/tmp/*"], network: ["localhost:*"] } }, allowedTools: ["Bash", "Write", "Read"], permissionMode: "acceptEdits" }});
for await (const message of result) { console.log(message);}When to Choose Python
- Your project is primarily in the Python ecosystem
- Integration with Python data science/ML libraries is needed
- Your team is more familiar with Python
- The explicit session management with
ClaudeSDKClientfits your use case
When to Choose TypeScript
- Your project is in the Node.js/frontend ecosystem
- You need full Hooks support (including SessionStart/End/Notification)
- You want to use the new V2 interface (cleaner API)
- You need stronger type safety (Zod schema validation)
- Building web applications or need JavaScript toolchain integration
Conclusion
Both SDKs are functionally equivalent for most use cases. Choose based on your tech stack and team familiarity:
- TypeScript has a slight edge if you need complete Hooks support or prefer the newer V2 preview interface
- Python is the natural choice for Python projects or when you need explicit session control via
ClaudeSDKClient