Claude Agent SDK Python Tutorial: Automate Coding Tasks Programmatically

If you've used Claude Code from the terminal, you already know it's one of the best AI coding assistants out there. But what if you want to go beyond the CLI? What if you need Claude Code running inside your Python scripts — automating refactors, generating tests, or orchestrating multi-step coding workflows without you babysitting a terminal?

That's exactly what the Claude Agent SDK is for. It's Anthropic's official Python SDK that wraps Claude Code into a programmable interface. You get the same powerful coding agent, but controlled from Python with full access to custom tools, hooks, and interactive conversations.

This tutorial walks you through everything: installation, basic queries, custom tools, and real-world automation patterns. By the end, you'll have a working setup that can automate coding tasks you'd normally do by hand.

What Is the Claude Agent SDK?

The Claude Agent SDK (formerly claude-code-sdk, now claude-agent-sdk) is a Python package that lets you invoke Claude Code programmatically. Under the hood, it spawns the Claude Code CLI as a subprocess and communicates with it via structured messages.

This matters because Claude Code isn't just a chat model — it's an agent that can read files, write code, run shell commands, and iterate on its own output. The SDK gives you programmatic control over all of that.

Key capabilities:

Prerequisites and Installation

You'll need three things:

  1. Python 3.10+
  2. Node.js (Claude Code CLI is a Node.js application)
  3. Claude Code CLI installed globally

First, install the Claude Code CLI:

npm install -g @anthropic-ai/claude-code

Then install the Python SDK:

pip install claude-agent-sdk

You'll also need an API key. Set it as an environment variable:

export ANTHROPIC_API_KEY=your-api-key-here

If you don't have direct Anthropic API access (or want to use a different provider), you can point Claude Code at any OpenAI-compatible endpoint by setting ANTHROPIC_BASE_URL. Services like KissAPI provide Claude API access through a single gateway — handy if you're in a region where Anthropic's direct API isn't available.

Basic Usage: Your First Query

The simplest way to use the SDK is the query() function. It's async and returns an iterator of messages:

import anyio
from claude_code_sdk import query, AssistantMessage, TextBlock

async def main():
    async for message in query(prompt="Write a Python function that checks if a number is prime"):
        if isinstance(message, AssistantMessage):
            for block in message.content:
                if isinstance(block, TextBlock):
                    print(block.text)

anyio.run(main)

This sends a prompt to Claude Code and prints the text response. Simple enough. But the real power comes when you let Claude use tools.

Letting Claude Read and Write Files

Claude Code's killer feature is that it can interact with your filesystem. With the SDK, you control exactly which tools it can use:

from claude_code_sdk import query, ClaudeCodeOptions

options = ClaudeCodeOptions(
    cwd="/path/to/your/project",
    allowed_tools=["Read", "Write", "Bash"],
    permission_mode="acceptEdits"  # auto-accept file changes
)

async for message in query(
    prompt="Read main.py and add type hints to all functions",
    options=options
):
    print(message)

The permission_mode="acceptEdits" flag is important for automation — without it, Claude Code would pause and ask for confirmation before writing files, which defeats the purpose of running it programmatically.

Available permission modes:

ModeBehaviorUse Case
defaultAsks for confirmation on writesInteractive use
acceptEditsAuto-accepts file edits, asks for BashAutomated refactoring
bypassPermissionsAuto-accepts everythingFully automated pipelines (use with caution)

Building Custom Tools

This is where things get interesting. You can define Python functions as tools that Claude can call during a conversation. The SDK runs them as in-process MCP servers — no separate process needed.

Here's a practical example: a tool that queries your database so Claude can write code that matches your actual schema:

from claude_code_sdk import (
    tool, create_sdk_mcp_server,
    ClaudeCodeOptions, ClaudeSDKClient
)

@tool("get_table_schema", "Get the schema of a database table", {"table_name": str})
async def get_table_schema(args):
    # In practice, query your actual database here
    schemas = {
        "users": "id INT PK, email VARCHAR(255), name VARCHAR(100), created_at TIMESTAMP",
        "orders": "id INT PK, user_id INT FK, total DECIMAL(10,2), status VARCHAR(20)",
    }
    schema = schemas.get(args["table_name"], "Table not found")
    return {"content": [{"type": "text", "text": schema}]}

@tool("list_tables", "List all database tables", {})
async def list_tables(args):
    return {"content": [{"type": "text", "text": "users, orders, products, sessions"}]}

server = create_sdk_mcp_server(
    name="db-tools",
    version="1.0.0",
    tools=[get_table_schema, list_tables]
)

options = ClaudeCodeOptions(
    mcp_servers={"db": server},
    allowed_tools=["Read", "Write", "mcp__db__get_table_schema", "mcp__db__list_tables"],
    cwd="/path/to/project"
)

async with ClaudeSDKClient(options=options) as client:
    await client.query("Write a SQLAlchemy model for the users table")
    async for msg in client.receive_response():
        print(msg)

Claude will call list_tables and get_table_schema on its own to understand your database, then write accurate model code. No hallucinated column names.

Real-World Automation Patterns

Pattern 1: Automated Test Generation

Point Claude at a source file and have it generate tests:

async def generate_tests(source_file: str, test_dir: str):
    options = ClaudeCodeOptions(
        cwd="/path/to/project",
        allowed_tools=["Read", "Write"],
        permission_mode="acceptEdits",
        max_turns=10
    )

    prompt = f"""Read {source_file} and generate comprehensive unit tests.
Write the tests to {test_dir}/test_{source_file.split('/')[-1]}.
Use pytest. Cover edge cases and error paths."""

    async for message in query(prompt=prompt, options=options):
        pass  # Claude writes the test file directly

# Usage
await generate_tests("src/auth.py", "tests/")

Pattern 2: Batch Code Review

Run Claude over a list of files and collect review comments:

async def review_files(files: list[str]) -> dict[str, str]:
    reviews = {}
    for file in files:
        options = ClaudeCodeOptions(
            cwd="/path/to/project",
            allowed_tools=["Read"],
            max_turns=3
        )

        result_text = []
        async for message in query(
            prompt=f"Review {file} for bugs, security issues, and code smells. Be specific and concise.",
            options=options
        ):
            if isinstance(message, AssistantMessage):
                for block in message.content:
                    if isinstance(block, TextBlock):
                        result_text.append(block.text)

        reviews[file] = "\n".join(result_text)
    return reviews

Pattern 3: CI/CD Integration

Run the SDK in your CI pipeline to auto-fix linting issues:

import subprocess
import anyio
from claude_code_sdk import query, ClaudeCodeOptions

async def auto_fix_lint():
    # Run linter first
    result = subprocess.run(
        ["ruff", "check", "src/", "--output-format=json"],
        capture_output=True, text=True
    )

    if result.returncode == 0:
        print("No lint issues found")
        return

    options = ClaudeCodeOptions(
        cwd="/path/to/project",
        allowed_tools=["Read", "Write"],
        permission_mode="acceptEdits",
        max_turns=15
    )

    prompt = f"""Fix these lint issues. Edit the files directly.
Don't change any logic, only fix the style/lint problems.

Lint output:
{result.stdout}"""

    async for message in query(prompt=prompt, options=options):
        pass

anyio.run(auto_fix_lint)

Using a Custom API Endpoint

The Claude Agent SDK uses Claude Code under the hood, which means it respects the same environment variables. If you're using an API gateway like KissAPI instead of the direct Anthropic API, just set:

export ANTHROPIC_BASE_URL=https://api.kissapi.ai
export ANTHROPIC_API_KEY=your-kissapi-key

Everything else works the same. This is particularly useful if you need to route through a gateway for cost tracking, rate limit management, or regional access.

Tips and Gotchas

query() vs ClaudeSDKClient

The SDK offers two interfaces:

Featurequery()ClaudeSDKClient
Use caseOne-shot tasksMulti-turn conversations
Custom toolsNoYes (MCP servers)
HooksNoYes
InteractiveNoYes
ComplexityLowMedium

Use query() for simple, fire-and-forget tasks. Use ClaudeSDKClient when you need custom tools, multi-turn conversations, or hooks that run before/after tool execution.

Need Claude API Access?

KissAPI provides Claude API access through an OpenAI-compatible endpoint. Pay-as-you-go, no subscription. Works with Claude Code, Cursor, and any OpenAI SDK.

Start Free →

What's Next

The Claude Agent SDK opens up a lot of possibilities. Some ideas to explore:

The SDK is still evolving — Anthropic recently renamed it from claude-code-sdk to claude-agent-sdk and added features like hooks and the ClaudeSDKClient class. Keep an eye on the GitHub repo for updates.