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

FeaturePythonTypeScript
Basic Queryquery() functionquery() function
Session ManagementClaudeSDKClient classBuilt into query()
Custom Tools@tool decoratortool() function + Zod schema
MCP Serverscreate_sdk_mcp_server()createSdkMcpServer()
Hooks SupportPartial (no SessionStart/End/Notification)Full support
Interrupt OperationsOnly with ClaudeSDKClientSupported
Streaming InputSupportedSupported
V2 InterfaceNoneAvailable (preview) - simplified send()/receive()
Type SafetyVia dataclassVia Zod + TypeScript types
Runtime RequirementsPython 3.10+Node.js 18+

Installation

Python

pip install claude-agent-sdk

TypeScript

npm install @anthropic-ai/claude-agent-sdk

API 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, ClaudeAgentOptions
import 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_server
from 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:

  • SessionStart
  • SessionEnd
  • Notification

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 asyncio
from 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 asyncio
from 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 seconds
setTimeout(() => {
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, ClaudeAgentOptions
from 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, HookContext
from 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 Any
import 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 tools
utilities_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, ClaudeAgentOptions
import 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 ClaudeSDKClient fits 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

References

Read Next

晒太阳的健康益处

Read Previous

Flutter Impeller 渲染引擎深度解析