Migration Guide: Python to Go

This guide helps Python developers transition to the Go Agent SDK. While the APIs differ due to language differences, the concepts are the same.


Quick Start Comparison

Python

python
import asyncio
from claude_agent_sdk import Agent

async def main():
    client = Agent()
    async for message in client.query("What is 2+2?"):
        print(f"{message.type}: {message.content}")

asyncio.run(main())

Go

go
package main

import (
    "context"
    "fmt"
    sdk "github.com/schlunsen/claude-agent-sdk-go"
    "github.com/schlunsen/claude-agent-sdk-go/types"
)

func main() {
    ctx := context.Background()
    messages, _ := sdk.Query(ctx, "What is 2+2?", nil)
    for message := range messages {
        switch m := message.(type) {
        case *types.AssistantMessage:
            fmt.Printf("Assistant: %v\n", m.Content)
        }
    }
}

Configuration Migration

Python uses dataclass constructors, Go uses a builder pattern with method chaining:

python
# Python: Dataclass constructor
options = AgentOptions(
    model="claude-opus-4-20250514",
    allowed_tools=["Bash", "Read", "Write"],
    system_prompt="You are a helpful assistant",
    max_turns=10,
)
go
// Go: Builder pattern
options := types.NewClaudeAgentOptions().
    WithModel("claude-opus-4-20250514").
    WithAllowedTools("Bash", "Read", "Write").
    WithSystemPrompt("You are a helpful assistant").
    WithMaxTurns(10)

Key difference: Go configuration is verified at compile-time, not runtime. Each With*() method returns the options object for chaining.


Query/Response Patterns

Pattern 1: Interactive Client with Session

python
async def main():
    client = Agent()
    await client.connect()

    await client.query("What is the capital of France?")
    async for msg in client.receive_response():
        messages.append(msg)

    await client.query("And Germany?")
    async for msg in client.receive_response():
        messages.append(msg)

    await client.close()
go
func main() {
    ctx := context.Background()
    client, _ := sdk.NewClient(ctx, options)
    defer client.Close(ctx)

    client.Connect(ctx)

    client.Query(ctx, "What is the capital of France?")
    for msg := range client.ReceiveResponse(ctx) {
        messages = append(messages, msg)
    }

    client.Query(ctx, "And Germany?")
    for msg := range client.ReceiveResponse(ctx) {
        messages = append(messages, msg)
    }
}

Hook System Migration

Python uses decorators, Go uses explicit hook callbacks in options:

python
# Python: Decorator-based hooks
@client.hook(HookEvent.PRE_TOOL_USE)
async def log_before_tool(input_data, hook_context):
    print(f"About to call: {hook_context.tool_name}")
    return {"continue": True}
go
// Go: Explicit hook matchers in options
preHook := types.HookMatcher{
    Matcher: stringPtr(".*"),
    Hooks: []types.HookCallbackFunc{
        func(ctx context.Context, input interface{},
            toolUseID *string, hookCtx types.HookContext,
        ) (interface{}, error) {
            fmt.Printf("About to call: %s\n", hookCtx.ToolName)
            return map[string]interface{}{"continue": true}, nil
        },
    },
}

options := types.NewClaudeAgentOptions().
    WithHook(types.HookEventPreToolUse, preHook)

Permission Handling

go
func checkPermissions(ctx context.Context,
    toolName string,
    input map[string]interface{},
    permCtx types.ToolPermissionContext,
) (interface{}, error) {
    if toolName == "Bash" {
        cmd := input["command"].(string)
        if strings.Contains(cmd, "rm -rf") {
            return &types.PermissionResultDeny{
                Behavior: "deny",
                Message:  "Dangerous command blocked",
            }, nil
        }
    }
    return &types.PermissionResultAllow{Behavior: "allow"}, nil
}

options := types.NewClaudeAgentOptions().
    WithCanUseTool(checkPermissions)

MCP Servers

go
// Go: Factory function for MCP servers
calculator, _ := types.NewSDKMCPServer("calculator",
    types.Tool{
        Name:        "add",
        Description: "Add two numbers",
        Handler: func(ctx context.Context, args map[string]any) (any, error) {
            a, _ := args["a"].(float64)
            b, _ := args["b"].(float64)
            return a + b, nil
        },
    },
)

options := types.NewClaudeAgentOptions().
    WithMCPServer("calculator", calculator)

Error Handling

Python uses try/except with exception classes. Go uses errors.As() type assertions:

go
messages, err := sdk.Query(ctx, "Hello", nil)

var permError *types.PermissionDeniedError
var cliError *types.CLINotFoundError

if errors.As(err, &permError) {
    fmt.Printf("Permission denied for %s\n", permError.ToolName)
} else if errors.As(err, &cliError) {
    fmt.Printf("CLI not found: %s\n", cliError.Message)
}

// Or use helper predicates
if types.IsPermissionDeniedError(err) { /* ... */ }
if types.IsCLINotFoundError(err) { /* ... */ }

Migration Checklist

TaskPythonGo
Async functionsasync deffunc
Await callsawait func()<-chan
Async iterationasync forfor ... range
ConfigurationOptions(key=val).WithKey(val)
Error handlingexcept ErrorTypeerrors.As(err, &var)
Hooks@client.hook().WithHook(event, matcher)
CallbacksAsync functionsSync functions
ContextException handlingcontext.Context
Concurrencyasyncio.gather()Goroutines + channels
Installpip installgo get

Performance Differences

AspectPythonGo
Startup~100ms~10ms
Memory overheadHigherLower
ConcurrencyLimited by GIL1M+ goroutines
DeploymentRuntime requiredSingle binary
Dev speedFaster prototypingSlower but safer

Further reading: Architecture Differences | Feature Parity