Anthropic SDK Intro
What Is the Anthropic Python SDK?
The Anthropic Python SDK is the official Python library for using Claude from your own programs. Instead of building web requests by hand, you call normal Python methods like client.messages.create(...). The library handles the network communication for you.
In technical terms, the SDK is a client library for the Claude API. The Claude API is the HTTP (REST) service Anthropic runs at https://api.anthropic.com. Each SDK method maps to one API endpoint. When you call a method, the SDK:
- builds an HTTP request,
- adds the required headers,
- sends the request,
- waits for the response, and
- turns the JSON it gets back into typed Python objects.
You work with Python functions and objects. The SDK deals with URLs, headers, status codes, and JSON parsing.
These are the names you will use throughout this tutorial:
| Item | Value | Where you use it |
|---|---|---|
| Package name on PyPI | anthropic | pip install anthropic |
| Module name | anthropic | import anthropic |
| Synchronous client class | Anthropic | from anthropic import Anthropic |
| Asynchronous client class | AsyncAnthropic | from anthropic import AsyncAnthropic |
| Default API key variable | ANTHROPIC_API_KEY | Read automatically from the environment |
| Required Python version | 3.10 or later | Check with python --version |
Note: The package, the module, and the class have related but different names. You install
anthropic, you importanthropic, and you create anAnthropicobject (capital A). Mixing these up is one of the most common beginner errors.
Terms Used in This Lesson
Several terms appear repeatedly in this lesson. Each one is explained below in plain language, followed by its technical meaning.
| Term | Simple meaning | Technical meaning |
|---|---|---|
| API | A doorway that lets one program ask another program to do something | Application Programming Interface: a defined set of operations, inputs, and outputs |
| REST API | An API you reach over the web with URLs | An HTTP-based API where each URL (endpoint) is a resource, and HTTP methods (GET, POST, …) are actions on it |
| Endpoint | One specific "address" of the API | A URL path plus HTTP method, such as POST /v1/messages |
| SDK | A ready-made toolkit that makes an API easy to use from one language | Software Development Kit: a library that wraps the API in language-native functions and types |
| Client | The object in your code that talks to the API | An instance (such as Anthropic()) that stores configuration and sends requests |
| JSON | The text format used to send data back and forth | JavaScript Object Notation: the request and response body format of the Claude API |
Why the SDK Exists
Any language that can send HTTP requests can call the Claude API. So why use a dedicated library? The best way to answer is to send the same request both ways and compare them.
Calling Claude Without the SDK
The script below calls the Messages endpoint directly with the general-purpose requests library (pip install requests):
import os
import requests
response = requests.post(
"https://api.anthropic.com/v1/messages",
headers={
"Authorization": f"Bearer {os.environ['ANTHROPIC_API_KEY']}",
"anthropic-version": "2026-10-21",
"content-type": "application/json",
},
json={
"model": "claude-opus-5-5",
"max_tokens": 1024,
"messages": [
{"role": "user", "content": "Explain what an SDK is in one sentence."}
],
},
timeout=60,
)
response.raise_for_status() # Raise an exception for 4xx/5xx responses
data = response.json() # Convert the JSON text into a Python dict
for block in data["content"]:
if block["type"] == "text":
print(block["text"])
This works, but your code now has to handle every protocol detail itself:
- The URL. You must know the base address and the exact path,
/v1/messages. - Authentication. You must read the key yourself and send it in the
Authorizationheader asBearer <key>. The olderx-api-key: <key>header is still accepted as an alternative. If you leave authentication out, the API rejects the request as unauthenticated. - The API version. The
anthropic-versionheader tells the API which version of its request and response format you expect. If you leave it out, the request is rejected. If you send an old value, you get the old behavior. You have to choose and maintain this value. - The content type. You declare that the body is JSON.
- Parsing. The response is a plain dictionary. A typo such as
data["contnet"]fails only at runtime with aKeyError, and your editor cannot warn you about it beforehand. - Failures.
raise_for_status()throws a genericHTTPError. To tell an invalid key apart from a rate limit, you have to inspect status codes yourself. - Reliability. If the network drops for a moment or the API is briefly overloaded, the request fails. Retrying with a delay is your job.
- Timeouts. You must choose a timeout. Without one,
requestscan wait indefinitely.
Every one of these lines is a place where bugs can appear. Every project that calls Claude would need to repeat them.
The Same Request With the SDK
from anthropic import Anthropic
client = Anthropic() # Reads ANTHROPIC_API_KEY from the environment
message = client.messages.create(
model="claude-opus-5-5",
max_tokens=1024,
messages=[
{"role": "user", "content": "Explain what an SDK is in one sentence."}
],
)
for block in message.content:
if block.type == "text":
print(block.text)
How this example works:
Anthropic()creates a client. No key is passed, so the SDK looks for theANTHROPIC_API_KEYenvironment variable. Keeping the key out of your source code is the recommended pattern.client.messages.create(...)sendsPOST /v1/messages. The SDK adds the URL, theAuthorizationheader, theanthropic-versionheader, and the content type automatically.- The keyword arguments
model,max_tokens, andmessagesare the same fields you put in the JSON body before. They are now Python parameters, so your editor can autocomplete and check them. messageis a typedMessageobject, not a dictionary. You readblock.typeandblock.textas attributes.- The loop goes through
message.contentbecause Claude's reply is a list of content blocks, not a single string. With current models, the list can begin with a thinking block (the model's internal reasoning) before the text block, so the loop prints only the blocks whose type is"text". Lesson 16 (Understand Message Blocks) covers this structure in detail.
The request sent over the network is essentially the same in both versions. The difference is how much of the plumbing your code must own.
Side-by-Side Comparison
| Concern | Raw HTTP (requests) | Anthropic SDK |
|---|---|---|
| Endpoint URL | You write it | Built in |
| Authentication header | You add Authorization: Bearer <key> | Added from ANTHROPIC_API_KEY or api_key= |
anthropic-version header | You choose and maintain it | Set automatically for your SDK version |
| Request parameters | Untyped dict | Typed parameters with editor autocomplete |
| Response | Plain dict | Typed objects (Pydantic models) |
| Errors | Generic HTTPError + status code | Specific exceptions (AuthenticationError, RateLimitError, …) |
| Retries | Write your own | Two automatic retries with exponential backoff by default |
| Timeouts | Choose your own (none by default in requests) | 10-minute default, configurable |
| Streaming | Parse server-sent events manually | Built-in helpers |
| Async support | Requires a different HTTP library | AsyncAnthropic with the same interface |
How the SDK Works Behind the Scenes
You do not need to know the internals to use the SDK. Knowing the general flow, however, makes errors and configuration options much easier to understand.
Your Python code
│ client.messages.create(model=..., max_tokens=..., messages=[...])
▼
Anthropic client
│ 1. Resolves the API key (argument or ANTHROPIC_API_KEY)
│ 2. Builds the request: URL, JSON body, headers
│ 3. Applies timeout and retry settings
▼
HTTP layer (httpx2)
│ 4. Sends the HTTPS request to api.anthropic.com
▼
Claude API
│ 5. Runs the model and returns a JSON response (or an error)
▼
Anthropic client
│ 6a. Success → parses JSON into a typed Message object
│ 6b. Retryable failure → waits, then retries automatically
│ 6c. Final failure → raises a specific Python exception
▼
Your Python code receives a Message object or catches an exception
Step by step:
- Authentication is resolved. If you passed
api_key=...to the constructor, the SDK uses it. Otherwise it readsANTHROPIC_API_KEY. This is whyAnthropic()with no arguments works once the environment variable is set. - The request is built. Your keyword arguments become the JSON body. The SDK adds the
anthropic-versionheader that matches its own major version (covered in the versioning section below). - Policies are applied. Each request inherits the client's timeout and retry settings, unless you override them for that call.
- The request is sent. The SDK uses
httpx2, an API-compatible fork of the popularhttpxHTTP library, for connections, connection pooling, and TLS. - The API responds. It returns either a successful JSON body or an error status code, such as 401 or 429.
- The response is converted. Successful responses become typed Python objects. Temporary failures, such as connection problems or 429/5xx responses, are retried. Errors that cannot be recovered become specific exceptions you can catch.
The Structure of the SDK: Client → Resource → Method
Every call in the SDK follows the same pattern:
client . resource . method ( parameters )
│ │ │
│ │ └── the action: create, list, retrieve, count_tokens, ...
│ └── the API area: messages, models, files, ...
└── your configured Anthropic / AsyncAnthropic instance
The pattern mirrors the API's URL structure. Once you know one endpoint, you can usually guess the matching SDK call:
| SDK call | HTTP endpoint | Purpose | Covered in |
|---|---|---|---|
client.messages.create(...) | POST /v1/messages | Send messages and get Claude's reply | Lessons 11–30 |
client.messages.stream(...) | POST /v1/messages (streaming) | Receive the reply piece by piece | Lessons 51–60 |
client.messages.count_tokens(...) | POST /v1/messages/count_tokens | Count input tokens before sending | Lesson 46 |
client.messages.batches.create(...) | POST /v1/messages/batches | Submit many requests for asynchronous processing | Lessons 131–140 |
client.models.list() | GET /v1/models | List available models | Lesson 12 |
client.files.upload(...) | POST /v1/files | Upload files for later use | Lessons 115–116 |
client.beta.… | Various | Access features that are still in beta | Advanced lessons |
Nested resources work the same way. batches lives under messages because its URL lives under /v1/messages/.
Why it is designed this way: a predictable structure makes the SDK discoverable. In an editor with autocomplete, typing client. lists the available resources, and typing client.messages. lists the available actions. You rarely need to memorize anything.
The Three Parameters You Will See Everywhere
Almost every example in this tutorial calls client.messages.create() with three required parameters:
message = client.messages.create(
model="claude-opus-5-5", # Which Claude model answers
max_tokens=1024, # The maximum length of the reply, in tokens
messages=[ # The conversation so far
{"role": "user", "content": "Hello, Claude"}
],
)
| Parameter | What it controls | What happens if you remove it |
|---|---|---|
model | The Claude model that processes the request. Models differ in capability, speed, and cost. | The call fails: the API requires a model. |
max_tokens | An upper limit on output length. A token is a small piece of text, roughly a word or part of a word. The model's internal thinking counts toward this limit, so do not set it too low. | The call fails: the API requires an explicit limit. |
messages | A list of turns, each with a role ("user" or "assistant") and content. | The call fails: there is nothing for Claude to respond to. |
Each of these gets its own lesson later (Choose a Claude Model, Set Max Tokens, Send User Messages). For now, the point is that the SDK expects these parameters, and the SDK's type definitions describe them.
Synchronous and Asynchronous Clients
The SDK includes two client classes with the same methods and parameters:
Anthropic: synchronous. Each call blocks until the response arrives. This is the simplest option and is ideal for scripts, notebooks, command-line tools, and traditional web frameworks such as Django or Flask views.AsyncAnthropic: asynchronous. Each call is a coroutine that youawait. While one request waits on the network, your program can do other work, such as running other requests.
Synchronous Example
from anthropic import Anthropic
client = Anthropic()
message = client.messages.create(
model="claude-opus-5-5",
max_tokens=1024,
messages=[{"role": "user", "content": "Name three uses of Python."}],
)
for block in message.content:
if block.type == "text":
print(block.text)
Asynchronous Example
import asyncio
from anthropic import AsyncAnthropic
async def main() -> None:
async with AsyncAnthropic() as client:
message = await client.messages.create(
model="claude-opus-5-5",
max_tokens=1024,
messages=[{"role": "user", "content": "Name three uses of Python."}],
)
for block in message.content:
if block.type == "text":
print(block.text)
asyncio.run(main())
What changed between the two versions:
- The class is
AsyncAnthropicinstead ofAnthropic. - The call is prefixed with
await, becausecreate()now returns a coroutine. - The code runs inside an
async deffunction, whichasyncio.run()starts. async withcloses the client's network connections when the block ends.
Everything else, including the method name, parameters, and response object, is identical. That is intentional: after you learn the synchronous API, the asynchronous one only adds await.
| Situation | Recommended client |
|---|---|
| Learning, scripts, notebooks, CLI tools | Anthropic |
| Django or Flask views (synchronous frameworks) | Anthropic |
FastAPI or other async frameworks | AsyncAnthropic |
| Sending many requests in parallel | AsyncAnthropic |
This tutorial uses the synchronous client until the Async SDK section (Lessons 71–80).
Typed Requests and Responses
The SDK is fully type-annotated:
- Request parameters are described with
TypedDicts. These are dictionaries whose allowed keys and value types are declared, so editors and type checkers can validate them. - Responses are Pydantic models. These are Python objects with declared fields, so you access data through attributes like
message.contentinstead of dictionary keys.
Inspecting a Response Object
from anthropic import Anthropic
client = Anthropic()
message = client.messages.create(
model="claude-opus-5-5",
max_tokens=1024,
messages=[{"role": "user", "content": "Say hello in French."}],
)
print(type(message)) # The Python class of the response
print(message.id) # Unique ID of this message
print(message.role) # Who wrote it: always "assistant"
print(message.model) # The model that produced it
print([block.type for block in message.content]) # The types of the content blocks
print(message.stop_reason) # Why Claude stopped generating
print(message.usage.input_tokens) # Tokens you sent
print(message.usage.output_tokens) # Tokens Claude generated
Example output (IDs, block types, token counts, and wording will differ for you):
<class 'anthropic.types.message.Message'>
msg_01XFDUDYJgAACzvnptvVoYEL
assistant
claude-opus-5-5
['text']
end_turn
14
9
What each field tells you:
message.iduniquely identifies the response. It is useful in logs.message.roleis"assistant"because the reply comes from Claude.message.modelconfirms which model actually answered.- The block types show what the reply contains. For a simple greeting this is often just
['text']. When the model reasons first, you will see['thinking', 'text']. message.stop_reasonexplains why generation ended.end_turnmeans Claude finished naturally.max_tokensmeans it hit your length limit. Lesson 20 covers all stop reasons.message.usagereports token counts, which determine cost.output_tokensincludes any thinking tokens. Lesson 30 covers this in depth.
Why Typed Objects Are Better Than Dictionaries
With a dictionary, a misspelled key such as data["stop_reasn"] is discovered only when the program runs. With a typed object, your editor can show the available attributes as you type, and a type checker can flag message.stop_reasn before you run anything.
Because the response is an object, dictionary-style access does not work:
message["content"] # TypeError: 'Message' object is not subscriptable
message.content # Correct
When you need a plain dictionary or JSON string, for example to store a response or return it from a web API, use the built-in helpers:
data = message.to_dict() # Python dict
json_text = message.to_json() # JSON-formatted string
What the SDK Handles for You
The SDK's most valuable features are the ones you never have to write yourself. Each is introduced briefly here and covered fully in its own lesson.
Authentication
import os
from anthropic import Anthropic
client = Anthropic() # Uses ANTHROPIC_API_KEY automatically
# Equivalent explicit form. This is useful when the key comes from somewhere else,
# such as a secrets manager:
client = Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])
Warning: Never write the key itself into your code, as in
Anthropic(api_key="sk-ant-..."). Code ends up in Git repositories, screenshots, and shared files, and anyone who has the key can use your account. Lessons 7, 8, and 171–173 cover safe key handling.
Specific Exceptions
When a request fails, the SDK raises an exception that describes the kind of failure:
| HTTP status | Exception | Typical cause |
|---|---|---|
| 400 | BadRequestError | Invalid parameters, such as a malformed messages list |
| 401 | AuthenticationError | Missing or invalid API key |
| 403 | PermissionDeniedError | The key is not allowed to perform this action |
| 404 | NotFoundError | Unknown resource, such as a wrong model name |
| 429 | RateLimitError | Too many requests in a short time |
| ≥ 500 | InternalServerError | A problem on the API side |
| — | APIConnectionError | The server could not be reached (network issue) |
The status-code exceptions all inherit from APIStatusError. So you can catch specific cases first and handle everything else with one generic branch:
import anthropic
from anthropic import Anthropic
client = Anthropic()
try:
message = client.messages.create(
model="claude-opus-5-5",
max_tokens=1024,
messages=[{"role": "user", "content": "Hello, Claude"}],
)
except anthropic.AuthenticationError:
print("Your API key is missing or invalid.")
except anthropic.RateLimitError:
print("Rate limited. Wait a moment and try again.")
except anthropic.APIConnectionError:
print("Could not reach the API. Check your internet connection.")
except anthropic.APIStatusError as error:
print(f"The API returned an error: {error.status_code}")
Why the order matters: Python checks except clauses from top to bottom. AuthenticationError and RateLimitError are subclasses of APIStatusError. If the APIStatusError branch came first, it would catch them, and the specific branches would never run. Always put more specific exceptions first.
Automatic Retries
Some failures are temporary. The SDK automatically retries the following 2 times by default, with a short exponential backoff (a delay that grows after each attempt):
- connection errors
- 408 Request Timeout
- 409 Conflict
- 429 Rate Limit
- 500-level errors
You can change this for the whole client or for one request:
from anthropic import Anthropic
client = Anthropic(max_retries=4) # Applies to every request from this client
client.with_options(max_retries=0).messages.create( # This request only
model="claude-opus-5-5",
max_tokens=1024,
messages=[{"role": "user", "content": "Hello, Claude"}],
)
Setting max_retries=0 disables retries. That is useful when you implement your own retry logic (Lessons 143 and 146) or when you want failures to surface immediately during testing.
Timeouts
By default, a request times out after 10 minutes. That is generous because long responses can take time to generate. You can set your own limit in seconds:
client = Anthropic(timeout=30.0) # All requests
client.with_options(timeout=10.0).messages.create(...) # One request
When a request times out, the SDK raises APITimeoutError, a subclass of APIConnectionError. Timed-out requests also count toward the automatic retries.
Note: For long outputs, streaming (Lessons 51–60) is the recommended approach. It is preferred over a very large timeout because some networks drop connections that stay idle too long.
Request IDs
Every response carries the ID that the API assigned to that request:
print(message._request_id) # e.g. req_018EeWyXxfu5pfWkrYcMdjWG
Log this value. If something goes wrong, the request ID lets Anthropic support find the exact request. Although the name starts with an underscore, _request_id is a public, documented property. Other underscore-prefixed attributes in the SDK are private and should not be relied on.
Closing Connections
The client keeps network connections open so later requests can reuse them. Connections are closed automatically when the client is garbage-collected. You can also close them explicitly with a context manager:
from anthropic import Anthropic
with Anthropic() as client:
message = client.messages.create(
model="claude-opus-5-5",
max_tokens=1024,
messages=[{"role": "user", "content": "Hello, Claude"}],
)
# The client's connections are closed here
SDK Version and API Version
Two separate version numbers are involved, and it helps to keep them apart:
- The SDK version is the version of the
anthropicPython package you installed, for example2.0.0. - The API version is the date-style value in the
anthropic-versionheader, for example2026-10-21. It tells the API which request and response format your code expects.
Each major version of the SDK sends one specific API version by default:
anthropic package version | API version sent |
|---|---|
| Earlier releases (before 2.0.0) | 2023-06-01 |
| 2.0.0 and later 2.x releases | 2026-10-21 |
Why it works this way: when Anthropic changes the API in a backward-incompatible way, it publishes a new API version instead of breaking existing integrations. Tying each SDK major version to one API version means a minor upgrade (2.0 → 2.1) will not quietly change the API behavior your code depends on. Moving to a new API version requires a deliberate major upgrade, and the official migration guide lists what changed.
To see which SDK version your environment is using:
import anthropic
print(anthropic.__version__)
If a feature from the documentation is missing, check this first. Your environment, such as a different virtual environment, may still be using an older package. Lesson 5 (Upgrade the SDK) covers upgrading.
Note: The SDK requires Python 3.10 or later. On an older interpreter,
pipwill not install a current release.
Where the SDK Fits in the Claude Ecosystem
Beginners often confuse the following products. They serve different purposes:
| Product | What it is | Who uses it | How you access Claude |
|---|---|---|---|
| Claude apps (web, desktop, mobile) | Ready-made chat interface | End users | Typing in a chat window |
| Claude API | HTTP service at api.anthropic.com | Developers in any language | HTTP requests with an API key |
| Anthropic Python SDK | Python library that calls the Claude API | Python developers | client.messages.create(...) |
| Other official SDKs | The same idea for TypeScript, Go, Java, C#, PHP, and Ruby | Developers in those languages | Language-native methods |
| Cloud provider clients | SDK classes that send requests through cloud platforms | Teams already on AWS, Google Cloud, or Azure | Same methods, different client class |
A subscription to the Claude apps and access to the Claude API are separate. API usage is billed per token through the Claude Console, where you also create API keys (Lesson 6).
The Python SDK also includes clients for cloud platforms. They expose the same messages.create() interface but authenticate through the cloud provider:
| Platform | Client class | Install extra |
|---|---|---|
| Amazon Bedrock | AnthropicBedrockMantle (AnthropicBedrock for the older InvokeModel path) | pip install "anthropic[bedrock]" |
| Claude Platform on AWS | AnthropicAWS (beta) | pip install "anthropic[aws]" |
| Claude Models on Google Cloud (Google managed) | AnthropicVertex | pip install "anthropic[vertex]" |
| Claude Models on Google Cloud (Anthropic managed) | AnthropicGoogleCloud (beta) | pip install "anthropic[google_cloud]" |
| Microsoft Foundry | AnthropicFoundry | None (included in the base package) |
Because the interface is shared, code written against Anthropic can usually move to a cloud client by changing only how the client is created. Lessons 161–170 cover this.
When to Use the SDK and When Not To
Use the SDK when:
- You are writing Python: scripts, data pipelines, Django/Flask/FastAPI backends, Jupyter notebooks, or automation tools.
- You want typed responses, specific exceptions, retries, and streaming without writing them yourself.
- You will use advanced features such as tool use, batches, or file uploads. The SDK includes helpers for these that would take significant effort to rebuild.
Consider a different approach when:
- You are not using Python. Use the official SDK for your language. If your language has none, call the HTTP API directly.
- You need a quick one-off test from the terminal. A single
curlcommand, or Anthropic'santcommand-line tool, may be faster than writing a script. - You are in a tightly restricted environment where you cannot install third-party packages. Raw HTTP with only the standard library is possible, but you then take on everything the SDK would have handled.
For almost every Python project, the SDK is the right default.
Practical Example: A First Look Script
This script combines the ideas in this lesson into a small, well-structured program. It checks the setup, sends a question, prints the answer, and reports useful metadata. It also handles the most common failures cleanly.
Setup
Install the SDK and set your API key. Lessons 4, 7, and 8 explain these steps in detail.
pip install anthropic
# macOS / Linux
export ANTHROPIC_API_KEY="your-api-key"
# Windows PowerShell
$env:ANTHROPIC_API_KEY = "your-api-key"
Save the following as first_look.py. Do not name it anthropic.py; the Common Mistakes section below explains why.
"""first_look.py: a first look at the Anthropic Python SDK."""
import os
import sys
import anthropic
from anthropic import Anthropic
MODEL = "claude-opus-5-5"
QUESTION = "In two sentences, what does a Python SDK for a web API do?"
def main() -> int:
print(f"anthropic SDK version: {anthropic.__version__}")
# 1. Fail early with a clear message if the key is not configured.
if not os.environ.get("ANTHROPIC_API_KEY"):
print("ANTHROPIC_API_KEY is not set.", file=sys.stderr)
return 1
# 2. Create the client and send the request.
try:
with Anthropic(max_retries=3, timeout=60.0) as client:
message = client.messages.create(
model=MODEL,
max_tokens=1024,
messages=[{"role": "user", "content": QUESTION}],
)
except anthropic.AuthenticationError:
print("The API key was rejected. Check that it is correct.", file=sys.stderr)
return 1
except anthropic.APIConnectionError:
print("Could not connect to the API.", file=sys.stderr)
return 1
except anthropic.APIStatusError as error:
print(f"API error {error.status_code}: {error.message}", file=sys.stderr)
return 1
# 3. Collect every text block into one string (thinking blocks are skipped).
answer = "".join(block.text for block in message.content if block.type == "text")
# 4. Show the answer and the metadata.
print()
print(answer or "(no text in this response)")
print("-" * 40)
print(f"Message ID: {message.id}")
print(f"Model: {message.model}")
print(f"Blocks: {[block.type for block in message.content]}")
print(f"Stop reason: {message.stop_reason}")
print(f"Input tokens: {message.usage.input_tokens}")
print(f"Output tokens: {message.usage.output_tokens}")
print(f"Request ID: {message._request_id}")
return 0
if __name__ == "__main__":
sys.exit(main())
Run it:
python first_look.py
Example output (Claude's wording, the block types, the IDs, and the token counts will differ for you):
anthropic SDK version: 2.0.0
A Python SDK wraps a web API in ordinary Python functions and objects, so you can call the service without building HTTP requests by hand. It also handles details like authentication, error handling, and converting responses into convenient data types.
----------------------------------------
Message ID: msg_01XFDUDYJgAACzvnptvVoYEL
Model: claude-opus-5-5
Blocks: ['thinking', 'text']
Stop reason: end_turn
Input tokens: 22
Output tokens: 118
Request ID: req_018EeWyXxfu5pfWkrYcMdjWG
How the Script Works
- Printing
anthropic.__version__confirms which SDK version is running. When behavior does not match the documentation, this is the first thing to check. - The environment check stops the program with a readable message if the key is missing. Without it, the failure would surface later as a less obvious authentication error.
Anthropic(max_retries=3, timeout=60.0)sets client-level policies. Temporary failures get three retries instead of two, and no request waits longer than 60 seconds.with ... as clientguarantees the client's connections are closed when the block ends, even if an exception occurs.- The
exceptchain goes from specific to general. A rejected key, a network problem, and any other API error each produce a different, actionable message.error.messageholds the error description from the API. max_tokens=1024leaves room for both the model's thinking and a two-sentence answer. With a very small limit, the thinking could use up the whole budget before any text is written (Lesson 13).- Collecting the answer joins all text blocks and skips thinking blocks. This is more robust than
message.content[0].text, which fails whenever the first block is a thinking block.answer or "(no text in this response)"prints a clear placeholder if there is no text at all. - The metadata lines show the fields you will use constantly: the block types, the message ID and request ID for debugging, the stop reason for detecting truncated replies, and the token counts for monitoring cost. Notice that
output_tokenscan be larger than the visible answer, because thinking tokens are counted too. sys.exit(main())returns an exit code of 0 for success or 1 for failure. That lets shell scripts and schedulers such as cron detect whether the run succeeded.
Try These Variations
| Change | What you will observe | What it teaches |
|---|---|---|
Set max_tokens=20 | Stop reason: max_tokens, with a cut-off answer, or (no text in this response) if thinking used the whole budget | max_tokens is a hard limit that includes thinking, and stop_reason tells you when it was hit |
| Set the API key to an invalid value | The AuthenticationError branch runs | 401 errors are not retried; they fail immediately |
Change MODEL to a made-up name | An APIStatusError branch runs (status 404) | Model names are validated by the API |
| Disconnect from the internet | The APIConnectionError branch runs after retries | Connection errors are retried before the exception is raised |
Common Mistakes
1. Naming Your File anthropic.py
What happens: You save your script as anthropic.py and run it. You get an error like:
ImportError: cannot import name 'Anthropic' from partially initialized module 'anthropic' (most likely due to a circular import)
Why: Python searches the script's own folder before installed packages. Your file named anthropic.py shadows the real SDK, so from anthropic import Anthropic imports your own file.
Fix: Rename the file, for example to first_look.py. Also delete any __pycache__/anthropic*.pyc left in that folder. Avoid naming files after any package you import, such as requests.py, json.py, or openai.py.
2. Confusing the Package, Module, and Class Names
What happens: Code like import Anthropic or client = anthropic() fails with ModuleNotFoundError or TypeError: 'module' object is not callable.
Why: anthropic (lowercase) is the module. Anthropic (capitalized) is the class inside it.
Fix: Use from anthropic import Anthropic, then client = Anthropic(). Or use import anthropic, then client = anthropic.Anthropic().
3. Treating message.content as a String, or Reading Only the First Block
What happens: print(message.content) prints something like [ThinkingBlock(...), TextBlock(text='...', type='text', ...)] instead of the answer. String operations such as message.content.upper() raise AttributeError. And message.content[0].text fails with AttributeError: 'ThinkingBlock' object has no attribute 'text' whenever the reply starts with a thinking block.
Why: A response can contain several content blocks of different types: thinking, text, tool calls, and others. So content is always a list of blocks, and the first block is not guaranteed to be text.
Fix: Read the text from the blocks by type:
text = "".join(block.text for block in message.content if block.type == "text")
4. Hardcoding the API Key
What happens: The key is written directly in the code and later committed to Git or shared. Anyone who finds it can spend your credits.
Why: It is the fastest way to "make it work" while learning.
Fix: Keep the key in the ANTHROPIC_API_KEY environment variable or in a .env file that Git ignores. If a key is ever exposed, revoke it in the Claude Console and create a new one.
5. Creating a New Client for Every Request
What happens: Code like Anthropic().messages.create(...) inside a loop or a web request handler creates a fresh client on every call.
Why it matters: Each client has its own connection pool. Creating a new one each time throws away open connections, which adds latency and wastes resources.
Fix: Create the client once, for example at module level or at application startup, and reuse it. Clients are designed to be long-lived.
6. Using the Synchronous Client Inside async Code
What happens: In an async application such as FastAPI, calling Anthropic().messages.create() from an async def endpoint blocks the event loop. Every other request waits until Claude responds.
Fix: In async code, use AsyncAnthropic and await its methods.
7. Following Documentation Written for a Different SDK Version
What happens: An example from a blog post or an older tutorial uses parameters or response fields that your installed version does not support, or that behave differently.
Fix: Check anthropic.__version__, compare it with the official documentation, and upgrade if needed. For major-version upgrades, read the migration guide before upgrading.
Best Practices
- Load credentials from the environment. Let
Anthropic()readANTHROPIC_API_KEY, or pass a key from a secrets manager. Never commit keys to source control. - Create one client and reuse it. Configure it once with your timeout and retry settings, and share it across your application.
- Use
with_options()for exceptions to the rule. Override timeouts or retries for specific calls instead of creating extra clients. - Catch specific exceptions first. Handle
AuthenticationError,RateLimitError, andAPIConnectionErrorseparately from the genericAPIStatusError, so each failure produces a useful response. - Read text by block type, never by position.
- Log
_request_idandusage. Request IDs make support cases solvable, and token counts make costs visible. - Check
stop_reason. A value ofmax_tokensmeans the answer was cut off, or missing. Do not treat it as a complete response. - Pin the SDK version in production. Record the version in
requirements.txt(for exampleanthropic==2.0.0, or a range such asanthropic>=2.0,<3) so that deployments are reproducible and major upgrades are deliberate. - Match the client to your framework. Use
Anthropicfor synchronous code andAsyncAnthropicforasynccode. - Use the official documentation as the source of truth. The API evolves quickly. When in doubt, check the Python SDK page and the API reference at
platform.claude.com/docs.
Exercise
Write a script named sdk_check.py that:
- Prints the installed SDK version.
- Creates a client with
max_retries=2and a 30-second timeout. - Asks Claude: "List three things a client library handles for developers."
- Prints only the text of the answer.
- Prints the stop reason and the total number of tokens used (input + output).
- Prints a friendly message instead of a traceback if the API key is invalid.
Then experiment:
- Run it with
max_tokens=15and check the stop reason and the printed text. - Set
ANTHROPIC_API_KEYtoinvalid-keyand confirm your friendly error message appears.
import anthropic
from anthropic import Anthropic
print(f"SDK version: {anthropic.__version__}")
client = Anthropic(max_retries=2, timeout=30.0)
try:
message = client.messages.create(
model="claude-opus-5-5",
max_tokens=1024,
messages=[
{
"role": "user",
"content": "List three things a client library handles for developers.",
}
],
)
except anthropic.AuthenticationError:
print("Your API key was rejected. Check ANTHROPIC_API_KEY.")
else:
answer = "".join(block.text for block in message.content if block.type == "text")
total_tokens = message.usage.input_tokens + message.usage.output_tokens
print(answer or "(no text in this response)")
print(f"Stop reason: {message.stop_reason}")
print(f"Total tokens: {total_tokens}")
How it works: The try/except/else structure runs the else block only when the request succeeds. This keeps success handling separate from error handling. Total tokens are the sum of the two usage fields, and the output count includes any thinking.
With max_tokens=15, the stop reason becomes max_tokens. The answer is either visibly cut off, or missing entirely (the script prints (no text in this response)) if the model spent the whole budget thinking. This is why real code uses comfortable limits such as 1,024 and always checks stop_reason.