Home

Chapter 1 · Lesson 0007

The Anthropic API — Claude and how it differs

30–40 min read · Hands-on coding

Motivation

Production AI engineering teams rarely commit to a single provider. OpenAI and Anthropic both power real products, often in the same codebase — one model for document analysis, another for conversation. A job posting that says “experience with LLM APIs” means both. If you can only call GPT-4o, you are half-equipped.

Claude is Anthropic’s model family, used at companies like Slack, Notion, and Quora. It handles long documents exceptionally well, is considered among the safest production models for regulated industries, and powers Claude Code — the tool you are using right now. This lesson teaches you to call it and to swap providers cleanly when a project demands it.

Prerequisites

  • Lesson 2: the OpenAI API call pattern — you will learn by analogy
  • Lesson 4: Pydantic models, model_validate_json()
  • An Anthropic API key — sign up at console.anthropic.com. Add it to your .env file as ANTHROPIC_API_KEY.
  • Install: uv add anthropic

Claude vs GPT — the practical differences

Both are capable LLMs that take text in and return text out. For most tasks, either works. The differences that actually matter in production:

Dimension GPT-4o / gpt-4o-mini Claude Sonnet 5
Context window 128K tokens 200K tokens
Long document handling Good Excellent — better at retrieving from the middle of long docs
Instruction following Very good Very good — particularly strong with structured XML prompts
Structured outputs Native .parse() support Native .parse() via output_format
Safety / refusals Moderate Conservative — prefers to decline ambiguous requests
Pricing (mid-tier model) ~$2.50 / $10 per M tokens ~$2.00 / $10 per M tokens (Sonnet 5)

The 200K context window is Claude’s most distinctive edge — you can fit entire codebases, legal contracts, or research papers into a single call.

The Anthropic API — Messages format

Anthropic’s Python SDK is called anthropic. The API is similar to OpenAI’s but with two structural differences: the system message is a separate top-level parameter (not in the messages list), and max_tokens is required (not optional).

import os
from dotenv import load_dotenv
import anthropic

load_dotenv()

client = anthropic.Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))

message = client.messages.create(
    model="claude-sonnet-5",
    max_tokens=1024,
    system="You are a helpful assistant. Be concise.",
    messages=[
        {"role": "user", "content": "What is the capital of France?"}
    ]
)

print(message.content[0].text)

Two differences from the OpenAI call:

The Claude model family

Anthropic uses a tiered naming system — Haiku (fast/cheap), Sonnet (balanced), Opus (most capable). As of 2026, the current generation is Claude 5:

Model ID Best for Rough cost (in/out per M)
claude-haiku-4-5-20251001 High-volume, simple tasks; classification; fast responses $1 / $5
claude-sonnet-5 The workhorse — most tasks; code generation; document analysis $2 / $10
claude-opus-5-5 Most capable; complex reasoning; research-grade tasks $4 / $20

Use claude-sonnet-5 as your default Anthropic model — the same role that gpt-4o-mini plays on the OpenAI side.

Anthropic ships a new model generation roughly every 12–18 months, and per-token prices shift with each one. Haiku's model ID above has stayed stable across generations, but Sonnet and Opus IDs change with each release — check claude.com/pricing for the current lineup rather than assuming this table stays accurate indefinitely.

Structured outputs with Claude

Claude supports native structured outputs through client.messages.parse() with a Pydantic output_format — the same interface you used for OpenAI in Lesson 4:

import os
from dotenv import load_dotenv
from pydantic import BaseModel
import anthropic

load_dotenv()
client = anthropic.Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))

class Invoice(BaseModel):
    invoice_number: str
    total_amount: float
    currency: str

response = client.messages.parse(
    model="claude-sonnet-5",
    max_tokens=512,
    messages=[{"role": "user", "content": "Invoice INV-001, total $500 USD."}],
    output_format=Invoice,
)

invoice = response.parsed_output
print(invoice.invoice_number)  # INV-001
print(invoice.total_amount)    # 500.0

response.parsed_output is a real Invoice instance with typed fields — identical to what OpenAI returns at response.choices[0].message.parsed.

System prompt fallback

If you need explicit schema control or want to understand how structured outputs work under the hood, the system prompt approach is still valid: generate a JSON Schema from your Pydantic model, embed it in the system prompt, and validate the response with model_validate_json():

import json
import os
from dotenv import load_dotenv
from pydantic import BaseModel
import anthropic

load_dotenv()
client = anthropic.Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))

class Invoice(BaseModel):
    invoice_number: str
    total_amount: float
    currency: str

schema = Invoice.model_json_schema()

message = client.messages.create(
    model="claude-sonnet-5",
    max_tokens=512,
    system=f"""Extract invoice details from the text.
Respond with a JSON object matching this schema exactly. No extra text.

Schema:
{json.dumps(schema, indent=2)}""",
    messages=[
        {"role": "user", "content": "Invoice INV-001, total $500 USD."}
    ]
)

raw_json = message.content[0].text
invoice = Invoice.model_validate_json(raw_json)
print(invoice.invoice_number)  # INV-001
print(invoice.total_amount)    # 500.0

Claude also supports structured output through its tool-calling API — define a “tool” whose input schema is your Pydantic model, and Claude will call it with structured arguments. The tool-calling API is covered in depth in Chapter 2 (Lesson 12).

Do not use output prefilling. Current Claude models (4.6 and later) return a 400 error if you include a pre-filled assistant message (e.g. starting the assistant turn with {"). Use client.messages.parse() with output_format, or the system prompt approach shown above.

Swapping providers cleanly

A well-designed AI application abstracts the LLM provider behind a simple interface — so you can switch from OpenAI to Anthropic without changing any of the calling code. Here is a clean pattern:

from abc import ABC, abstractmethod

class LLMClient(ABC):
    @abstractmethod
    def complete(self, system: str, user: str, max_tokens: int = 512) -> str:
        ...

class OpenAIClient(LLMClient):
    def __init__(self):
        from openai import OpenAI
        import os
        self.client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))

    def complete(self, system: str, user: str, max_tokens: int = 512) -> str:
        response = self.client.chat.completions.create(
            model="gpt-4o-mini",
            max_tokens=max_tokens,
            messages=[
                {"role": "system",  "content": system},
                {"role": "user",    "content": user},
            ],
        )
        return response.choices[0].message.content

class AnthropicClient(LLMClient):
    def __init__(self):
        import anthropic
        import os
        self.client = anthropic.Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))

    def complete(self, system: str, user: str, max_tokens: int = 512) -> str:
        message = self.client.messages.create(
            model="claude-sonnet-5",
            max_tokens=max_tokens,
            system=system,
            messages=[{"role": "user", "content": user}],
        )
        return message.content[0].text

# Usage — identical regardless of provider:
def get_client(provider: str) -> LLMClient:
    if provider == "openai":
        return OpenAIClient()
    elif provider == "anthropic":
        return AnthropicClient()
    raise ValueError(f"Unknown provider: {provider}")

client = get_client("anthropic")
result = client.complete(
    system="You are a helpful assistant.",
    user="What is the capital of Japan?",
)
print(result)

This pattern appears throughout Chapter 2 and Chapter 3. The calling code never imports openai or anthropic directly — it only uses the LLMClient interface. Swapping providers is a one-line change.

Assignment

  1. Read: How To Choose an LLM by Matt Pocock (aihero.dev) — about 10 minutes. A practical decision framework for choosing between providers and model tiers. Directly applicable to every project in this curriculum where you have to pick a model.
  2. Do: Add Anthropic support to your CLI assistant from Project 1.
    1. Add ANTHROPIC_API_KEY to your .env file
    2. Run uv add anthropic
    3. Implement the LLMClient abstraction from this lesson (both OpenAIClient and AnthropicClient)
    4. Add a --provider flag to assistant.py: --provider openai (default) or --provider anthropic
    5. Test both providers with the same question and compare responses

    Expected output: uv run assistant.py --provider anthropic "Explain what RAG is in one sentence." returns a Claude response. The provider-swap pattern is used in real products to A/B test models, fall back when one provider is down, and route different task types to the best model.

Knowledge Check

Answer each question from memory, then click to reveal.

Additional Resources