Back to Blog

Getting Started with the Claude Agent SDK for Python

March 15, 20268 min readMichael Ridland

I've been building AI agents with various frameworks for the past couple of years. LangChain, Semantic Kernel, OpenAI's Agents SDK, AutoGen - the list keeps growing. Each has its own philosophy about how agents should work, and each introduces its own abstractions that you need to learn before you can do anything useful.

The Claude Agent SDK takes a different approach. It's thin. You write Python, you call a function, you get streaming responses. There's no agent graph to define, no state machine to configure, no fifteen-layer abstraction between you and the model. After spending time with it on several client projects, I think this simplicity is its biggest strength.

The official Python SDK documentation is thorough and well-organised. Let me share what I've found most useful in practice and where the SDK really shines.

Installation and First Impressions

pip install claude-agent-sdk

That's it. No dependency tree that takes five minutes to resolve. No conflicts with your existing packages. It installs cleanly and you're ready to go.

The SDK gives you two main ways to interact with Claude: query() for one-off tasks, and ClaudeSDKClient for ongoing conversations. Understanding when to use which will save you time and frustration.

query() - For Tasks That Don't Need Memory

query() creates a fresh session every time you call it. No conversation history, no memory of previous interactions. Each call is independent.

This is perfect for automation scripts where each task stands alone. Process a document. Analyse a log file. Generate a code review. These are discrete tasks where you don't need the agent to remember what it did five minutes ago.

Here's a practical example:

import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions

async def review_code(file_path: str):
    options = ClaudeAgentOptions(
        system_prompt="You are a senior Python developer reviewing code for bugs and style issues.",
        permission_mode="acceptEdits",
        cwd="/home/user/project",
    )

    async for message in query(prompt=f"Review the file at {file_path}", options=options):
        print(message)

asyncio.run(review_code("src/main.py"))

The options parameter lets you configure the session. system_prompt shapes the agent's behaviour. permission_mode controls what the agent can do (read files, edit files, run commands). cwd sets the working directory.

One thing I appreciate - the response streams. You get messages as they arrive rather than waiting for the entire response to complete. For user-facing applications, this makes the agent feel responsive. For automation, you can process outputs incrementally.

ClaudeSDKClient - When Context Matters

If your agent needs to remember previous exchanges - follow-up questions, multi-step tasks, interactive sessions - use ClaudeSDKClient. It maintains a session across multiple interactions.

Think of it this way: query() is like sending a fresh email to someone each time. ClaudeSDKClient is like having an ongoing Slack thread. The second approach lets you build on previous messages, reference earlier context, and have the agent track what it's already done.

The practical difference shows up in scenarios like:

  • "Analyse this CSV file" followed by "Now create a chart from the results"
  • Debugging sessions where you try one fix, see what happens, then try another
  • Interactive data exploration where each question builds on the last

ClaudeSDKClient also supports interrupts, which query() doesn't. If the agent is mid-response and you need to redirect it, you can. Useful for interactive applications where users change their mind.

Custom Tools - This Is Where It Gets Good

The @tool decorator is clean. You define a tool with a name, description, and input schema, then write a function that does the work. The agent decides when to call your tool based on the user's request.

from claude_agent_sdk import tool
from typing import Any

@tool("check_stock", "Check current stock levels for a product", {"product_id": str})
async def check_stock(args: dict[str, Any]) -> dict[str, Any]:
    # Your actual stock checking logic here
    stock_level = await get_stock_from_database(args["product_id"])
    return {
        "content": [
            {"type": "text", "text": f"Product {args['product_id']}: {stock_level} units in stock"}
        ]
    }

The input schema supports two formats. For simple tools, use the dictionary shorthand - {"name": str, "count": int}. For complex validation with minimum values, required fields, and nested objects, use full JSON Schema format. I tend to use the shorthand for most tools and only pull out JSON Schema when I need proper validation.

What I like about this approach compared to some other frameworks: there's no tool class to inherit from, no registry to manage, no special initialisation. You decorate a function and it becomes a tool. Add it to your server and the agent can use it.

Building MCP Servers In-Process

This is one of the SDK's more interesting features. You can create an MCP (Model Context Protocol) server that runs inside your Python process. No separate server to deploy, no HTTP endpoints to manage.

from claude_agent_sdk import tool, create_sdk_mcp_server, ClaudeAgentOptions

@tool("search_tickets", "Search support tickets", {"query": str, "status": str})
async def search_tickets(args: dict[str, Any]) -> dict[str, Any]:
    results = await ticket_db.search(args["query"], status=args["status"])
    return {"content": [{"type": "text", "text": format_tickets(results)}]}

@tool("create_ticket", "Create a new support ticket", {"title": str, "description": str, "priority": str})
async def create_ticket(args: dict[str, Any]) -> dict[str, Any]:
    ticket = await ticket_db.create(
        title=args["title"],
        description=args["description"],
        priority=args["priority"]
    )
    return {"content": [{"type": "text", "text": f"Created ticket #{ticket.id}"}]}

support_server = create_sdk_mcp_server(
    name="support-tools",
    version="1.0.0",
    tools=[search_tickets, create_ticket],
)

options = ClaudeAgentOptions(
    mcp_servers={"support": support_server},
    allowed_tools=["mcp__support__search_tickets", "mcp__support__create_ticket"],
)

The tool naming convention for MCP tools is mcp__<server_name>__<tool_name>. You need to include the tools you want available in allowed_tools, which acts as a whitelist. The agent can only call tools you've explicitly permitted.

For a client in the professional services space, we built an MCP server with tools for searching their knowledge base, checking project status, and drafting client communications. The agent could answer questions like "What's the status of the Morrison project?" by calling the project status tool, then draft an update email using the knowledge base tool to pull relevant context. All running in a single Python process.

Tool Annotations

The @tool decorator accepts optional annotations that tell the agent about a tool's characteristics:

from mcp.types import ToolAnnotations

@tool(
    "delete_record",
    "Delete a database record permanently",
    {"record_id": str},
    annotations=ToolAnnotations(
        readOnlyHint=False,
        destructiveHint=True,
        openWorldHint=False,
    )
)
async def delete_record(args: dict[str, Any]) -> dict[str, Any]:
    # deletion logic
    ...

readOnlyHint tells the agent whether the tool just reads data or modifies something. destructiveHint flags tools that can't be undone. openWorldHint indicates whether the tool accesses external systems. These annotations influence how cautiously the agent uses the tool. A tool marked as destructive gets extra confirmation before the agent calls it.

Session Management

list_sessions() lets you retrieve previous sessions. You can filter by project directory and limit results. This is useful for building interfaces where users can return to previous conversations or for auditing what the agent did.

from claude_agent_sdk import list_sessions

sessions = list_sessions(
    directory="/home/user/project",
    limit=10,
    include_worktrees=True,
)

for session in sessions:
    print(f"Session {session.id}: {session.created_at}")

The include_worktrees parameter controls whether sessions from git worktrees are included. Handy if you're using worktrees for branch isolation during development.

Streaming Input

Both query() and ClaudeSDKClient accept streaming input through async iterables. Instead of passing a complete string, you can stream prompts in chunks:

async def stream_prompt():
    yield {"type": "text", "text": "Analyse this "}
    yield {"type": "text", "text": "log file for errors"}

async for message in query(prompt=stream_prompt()):
    print(message)

This matters when your input comes from another stream - reading a large file, receiving data from a WebSocket, processing real-time events. You don't need to buffer everything into a string first.

Hooks

The SDK supports hooks - callbacks that run in response to specific events during the agent's execution. Tool calls, message events, that kind of thing. Hooks let you add logging, validation, or custom logic without modifying the agent's core behaviour.

This is where you'd add things like: log every tool call to your audit system, validate that the agent isn't trying to access files outside a permitted directory, or trigger notifications when certain conditions are met.

Practical Patterns We Use

Batch processing with query(). When you need to process a list of items independently, spin up multiple query() calls. Each one gets its own session and can run concurrently with asyncio. We've used this pattern to review pull requests, process document batches, and generate reports across multiple data sources.

Interactive agents with ClaudeSDKClient. For internal tools where users chat with an agent through a web interface, ClaudeSDKClient maintains the conversation state. The user can ask follow-up questions, change direction, and build on previous answers.

Tool composition. Start with a few simple tools and let the agent compose them. An agent with search_documents, summarise_text, and send_email tools can handle "Find our latest proposal for Client X and email a summary to the project manager" without you writing any orchestration logic. The agent figures out the tool sequence on its own.

Where the SDK Fits in Our Stack

We use the Claude Agent SDK alongside other tools depending on the client's needs. For Microsoft-heavy organisations, we pair it with Azure AI Foundry for model management and deployment. For organisations building multi-agent systems, we sometimes combine it with OpenClaw for the channel routing and orchestration layer.

The SDK doesn't try to be everything. It's a clean interface for building agents that use Claude as their reasoning engine, with your custom tools for the domain-specific work. That focus is refreshing in a space where most frameworks try to solve every problem at once and end up solving none of them well.

At Team 400, we build production AI agents for Australian organisations using Claude, Azure OpenAI, and other platforms. If you're evaluating agent frameworks or need help building agents that connect to your business systems, we work across the full stack from AI strategy through to production agent development.

The Claude Agent SDK is one of the cleaner options available right now. If you're a Python team looking to build agents without adopting a heavyweight framework, it's worth a serious look.