The Anthropic Python SDK Mental Model
The official anthropic Python package is smaller than it looks from the outside.
Underneath every method call, decorator, and streaming context manager sits one HTTP endpoint: POST /v1/messages.
The SDK's job is to make calling that endpoint safe, typed, and ergonomic from Python, in both blocking scripts and concurrent services.
Once you see the package this way, the rest of its surface area, sync versus async clients, retry configuration, and streaming, stops looking like a pile of unrelated features and starts looking like a small number of decisions layered on top of one request.
Summary
- Core Idea: The
anthropicpackage is a typed HTTP client for the Messages API, offered as two parallel client classes that differ only in concurrency model. - Why It Matters: Talking to the API directly with
requestsorhttpxmeans reimplementing retries, streaming parsing, and response typing yourself; the SDK gives you all three for free. - Key Concepts: client, sync vs async, retry policy, timeout, streaming, response model.
- When to Use: Any Python code that calls Claude, from a one-off script to a production FastAPI service handling hundreds of concurrent requests.
- Limitations / Trade-offs: The SDK does not hide the underlying API's shape. You still need to understand messages, tokens, and stop reasons; the SDK just removes the plumbing around them.
- Related Topics: Messages API request/response shape, retry and timeout configuration, streaming events, type-hinted response models.
Foundations
One endpoint, two doors
Every call you make with the anthropic package, whether it looks like client.messages.create(...) or client.messages.stream(...), ultimately sends a POST request to /v1/messages with a JSON body containing a model, max_tokens, and a list of messages.
The SDK gives you two client classes as entry points to that same endpoint:
anthropic.Anthropic()is the synchronous client. Calling it blocks the current thread until a response (or the first streamed chunk) arrives.anthropic.AsyncAnthropic()is the asynchronous client. Calling it returns a coroutine youawait, so your program can do other work while the request is in flight.
Both classes expose the same methods, the same parameters, and the same response types. Picking one over the other does not change what you can ask Claude to do. It changes how your Python program behaves while waiting for the answer.
The client as a small, reusable object
A client instance is not a single request. It is a configured entry point you construct once and reuse.
import anthropic
client = anthropic.Anthropic() # reads ANTHROPIC_API_KEY from the environmentConstruction is where you set things that should apply to every request made through that client: the API key, a custom base_url, default timeout, and default max_retries. You then call client.messages.create(...) or client.messages.stream(...) many times against the same client, rather than building a new one per request.
This matters because retries, connection pooling, and timeout behavior are properties of the client, not of an individual call. A client you construct once at application startup and reuse is the normal pattern; constructing a fresh client inside a hot request-handling loop just throws away that reuse for no benefit.
Mechanics & Interactions
Sync and async are the same contract, different runtimes
Because Anthropic() and AsyncAnthropic()share the same method names and parameters, moving code between them is mostly mechanical: add async to the function signature, await the call, and use async with instead of with for the streaming context manager.
# Sync
response = client.messages.create(model="claude-sonnet-5", max_tokens=1024, messages=[...])
# Async
response = await async_client.messages.create(model="claude-sonnet-5", max_tokens=1024, messages=[...])What differs is what happens in your program while the request is outstanding. The sync client parks the calling thread. The async client yields control back to the event loop, so other coroutines, other in-flight API calls, database queries, incoming web requests, can make progress at the same time.
This is why the decision between the two clients is really a decision about your program's concurrency model, not about the Claude API itself. A script that makes one request and prints the answer has nothing to gain from async. A web service fielding many simultaneous requests, each of which needs to call Claude, gains a great deal from it, because a single event loop can juggle hundreds of outstanding API calls without hundreds of OS threads.
Retries sit between you and the network
Real networks fail in ways that have nothing to do with your prompt: a connection drops, the API returns a 529 because it is temporarily overloaded, a 429 arrives because you briefly exceeded a rate limit. The SDK's retry layer exists to absorb exactly this class of failure without you writing a retry loop by hand.
By default, both clients retry a request automatically a small number of times, with a backoff delay between attempts, whenever the failure looks transient (network errors, 408, 409, 429, and 5xx responses). Failures that are not transient, a malformed request, an invalid API key, a model that does not exist, are not retried, because retrying them would just reproduce the same error.
This behavior is configurable per client (max_retries=... at construction) and per request (with_options(...)), which lets you tighten retries for latency-sensitive calls and loosen them for background jobs. The full parameter reference lives in a dedicated page (see Related).
Streaming turns one response into many events
A non-streaming call waits for the entire response before returning anything. For short answers this is fine. For long ones, a multi-paragraph explanation, a large code generation, waiting for the full response before showing the user anything makes an application feel slow even when the model is working at a normal pace.
Streaming changes the shape of the interaction from "one request, one response" to "one request, many incremental events." Instead of returning a completed Message object, client.messages.stream(...) returns a context manager that exposes an iterator, most commonly consumed through its text_stream helper, yielding text as the model generates it.
Non-streaming: request ──────────────────► [ full response ]
Streaming: request ─► chunk ─► chunk ─► chunk ─► [ done ]
Conceptually, streaming does not change what the model produces. It changes when you get to see pieces of it. The same final message, the same token usage, the same stop reason are all still available at the end of the stream; you have just been given the option to render output as it arrives instead of waiting.
Advanced Considerations & Applications
Where the three concepts meet
The reason sync/async, retries, and streaming belong in one mental model is that they interact.
A retried request under streaming, for example, is more subtle than a retried non-streaming request: if a connection drops mid-stream after some text has already been shown to a user, simply retrying the whole request from scratch risks duplicating output the user already saw. The SDK handles the common transient-failure cases here, but you should understand this interaction before building a production streaming feature, rather than discovering it during an incident.
Async and streaming also compose directly: an AsyncAnthropic client's streaming context manager is an async context manager, iterated with async for, which lets a single async application stream responses to many concurrent clients (for example, many open WebSocket connections) without dedicating a thread to each one.
| Approach | Strength | Weakness | Best Fit |
|---|---|---|---|
| Sync, non-streaming | Simplest code, easiest to reason about | Blocks until the full response arrives | One-off scripts, batch jobs, notebooks |
| Sync, streaming | Renders output incrementally with simple control flow | Still blocks the thread between chunks | CLI tools, single-user desktop apps |
| Async, non-streaming | Frees the event loop while waiting | Adds async/await complexity | Backend services handling many independent requests |
| Async, streaming | Renders incrementally and handles many concurrent users | Most complex to reason about and debug | Chat UIs, multi-user web services, agent loops |
Choosing a starting point
A reasonable default: reach for Anthropic() first, because it is the simplest thing that works, and move to AsyncAnthropic() only once you have a concrete concurrency requirement, typically a web framework (like FastAPI) that is already async, or a workload that fans out many simultaneous Claude calls. Add streaming when the response is long enough, or the interaction is interactive enough, that showing partial output measurably improves the experience.
None of these are one-way doors. Because the method surface is shared between the two client classes, code written against the sync client translates to the async client with mechanical, low-risk changes rather than a rewrite.
Common Misconceptions
- "AsyncAnthropic() is faster." It is not faster per request; a single async call to Claude takes the same amount of time as the same call made synchronously. What async buys you is the ability to have many requests in flight at once without one thread per request.
- "Streaming lets you get a partial answer if you cancel early." True in a narrow sense, but streaming does not change what the model computes; it changes when you receive it. Canceling a stream early still means you asked for (and were billed for, up to that point) work the model already did.
- "Retries mean I never need to handle errors." The SDK retries transient failures automatically, but a request that ultimately fails after retries still raises an exception. Retries reduce how often you see transient errors; they do not eliminate error handling.
- "I should always use the async client for production code." Production is not synonymous with async. A production batch pipeline that processes documents one at a time has no concurrency to exploit, and the sync client is the simpler, equally correct choice there.
FAQs
Do Anthropic() and AsyncAnthropic() support different features?
No.
Both clients expose the same methods (messages.create, messages.stream, and so on) with the same parameters and return the same response types.
The only difference is that one blocks the calling thread and the other is awaited inside an event loop.
Can I use both clients in the same application?
Yes, though it is uncommon.
A typical case is a mostly-async web service that also has a synchronous startup or maintenance script using Anthropic() directly, outside the event loop.
Does streaming change the final result I get from the model?
No.
The same text, token usage, and stop reason are produced whether or not you stream; streaming only changes whether you see the output incrementally or all at once at the end.
What counts as a "transient" failure that gets retried automatically?
Network-level connection errors, and HTTP responses in the 408, 409, 429, and 5xx ranges.
These represent problems on the network or server side that are likely to succeed if you simply try again.
Will the SDK retry a request that fails because of a bad prompt or invalid parameters?
No.
Client errors like an invalid model name or a malformed request return a 4xx error (excluding 408/409/429) and are not retried, because retrying an unchanged malformed request would just fail again.
Is the client object thread-safe to reuse across requests?
Yes, a single client instance is designed to be constructed once and reused for many requests, which is also what allows connection pooling and configured retry/timeout behavior to actually take effect.
Why would I choose the sync client for a web application?
If your web framework is itself synchronous (classic WSGI Flask or Django views without async support), the sync client is the natural match. Bolting an async client onto a sync framework adds complexity without a concurrency benefit.
Does streaming require the async client?
No.
Both clients support streaming; Anthropic().messages.stream(...) and AsyncAnthropic().messages.stream(...) both exist, iterated with for and async for respectively.
What happens to token usage and billing when I stream?
It is unaffected by streaming.
You are billed for the tokens the model actually generates, whether you receive them as one final response or as many incremental chunks.
If I cancel a stream partway through, does that stop the model from generating more tokens?
It stops your client from receiving further chunks, but whether the underlying generation stops immediately depends on how you cancel (closing the stream context vs. letting the connection continue). Treat mid-stream cancellation as best-effort, not an instant hard stop.
Do I need to parse raw JSON out of the streaming response myself?
No.
The SDK parses the underlying server-sent events for you and exposes typed accessors like text_stream, as well as the fully assembled final message once the stream completes.
Is one client "more official" or more production-ready than the other?
No, both are fully supported, GA parts of the same package.
Neither is a beta or fallback version of the other; they are two interfaces to the same underlying API.
Related
- Python SDK Basics - install the SDK, construct a client, and send your first request.
- Choosing Between Anthropic() and AsyncAnthropic() in Production Code - a deeper, decision-focused comparison for FastAPI services and scripts.
- Streaming Responses with the Python SDK - working code for
client.messages.stream()andtext_stream. - Python SDK Retry and Timeout Configuration Reference - the full
max_retries,timeout, andwith_options()reference.
Stack versions: Written against the Claude model lineup current as of ~June 2026 - Claude Fable 5, Claude Opus 4.8, Claude Sonnet 5 (the default), and Claude Haiku 4.5 - and the official
anthropicPython SDK (latest 0.x release). Model names, SDK versions, and pricing move quickly - verify current specifics at platform.claude.com/docs before relying on them.