Python SDK Best Practices
A field-tested checklist for using the anthropic Python SDK well in production: client setup, sync/async choice, retries, streaming, and error handling.
How to Use This Checklist
- Treat each item as a positive rule - the thing to do, not just a mistake to avoid.
- Work through it once when standing up a new service that calls Claude, then revisit section D whenever you add a new failure mode to production.
- Pair this with the dedicated pages linked in Related for the full explanation behind any single rule.
A - Client Setup
- Construct the client once and reuse it. Build
Anthropic()orAsyncAnthropic()at module or application startup, not inside a request handler or loop body - reconstructing it per call discards connection pooling. - Read the API key from the environment by default. Let
Anthropic()pick upANTHROPIC_API_KEYautomatically rather than hardcoding a key string in source; passapi_key=...explicitly only when the key comes from a secrets manager at runtime. - Pin the SDK version in your dependency file. An unpinned
anthropicdependency can introduce new response fields or block types between deploys; pin it and upgrade deliberately. - Set
max_retriesandtimeoutdeliberately, not by accident. Decide values based on whether the call is interactive (tight timeout, few retries) or a background job (looser timeout, more retries), rather than leaving every call site on the same defaults. - Only reach for a custom
http_clientwhen you have a real infrastructure requirement. Proxies, custom CA bundles, and connection-pool tuning are valid reasons; don't add the complexity speculatively.
B - Sync vs Async
- Match the client to your program's concurrency model, not the other way around. Use
Anthropic()in scripts, CLIs, and sync frameworks; useAsyncAnthropic()in async web services and anywhere you fan out concurrent calls. - Never call the sync client directly inside an
async defroute. It blocks the entire event loop for the call's duration; useAsyncAnthropic()in async handlers, or wrap unavoidable sync calls in a thread pool. - Use
asyncio.gatherto actually get concurrency out of the async client. Awaiting each call sequentially inside a loop gives up the throughput benefit that motivated choosingAsyncAnthropic()in the first place. - Bound concurrent fan-out with a semaphore. Unbounded
asyncio.gatherover many requests can overwhelm your own rate limits; cap it to a sensible concurrency limit for the workload. - Don't switch to async for latency reasons alone. A single request takes the same wall-clock time on either client; async pays off only when multiple requests overlap.
C - Streaming
- Stream any request where
max_tokensis large. Above roughly 16,000 output tokens, a non-streaming call risks a client-side HTTP timeout;client.messages.stream()avoids that risk regardless of output size. - Use
text_streamfor the common case, raw events only when you need them.text_streamhandles the parsing for plain text; drop to raw event iteration only when you need tool-use or thinking events mid-stream. - Call
get_final_message()after the loop, not before. It returns the same complete, typedMessagea non-streaming call would, includingstop_reasonandusage- don't reconstruct that information by hand from accumulated chunks. - Match
with/async withto the client you're using.Anthropic()streaming useswithandfor;AsyncAnthropic()streaming usesasync withandasync for. Mixing them raises aTypeError. - Wrap streaming calls in the same error handling as non-streaming ones. A dropped connection surfaces as an exception from inside the iteration, not before it starts; don't assume streaming calls are exempt from the retry/exception patterns you use elsewhere.
D - Error Handling
- Catch typed exceptions, most specific first. Chain
except anthropic.NotFoundError,except anthropic.RateLimitError,except anthropic.APIStatusError,except anthropic.APIConnectionErrorrather than one broadexcept Exception. - Don't build custom retry logic for what the SDK already retries. Network errors,
429,5xx,408, and409are retried automatically up tomax_retries; add your own handling for what happens after retries are exhausted, not to duplicate them. - Never retry a
400,401,403,404, or422unchanged. These are non-retryable by design - the request itself needs to change, not just be resent. - Log
e.status_code,e.message, ande.typeonAPIStatusError, not the raw exception string. The.typefield gives finer-grained classification than the HTTP status alone (distinguishingrate_limit_errorfromoverloaded_error, for example). - Fail fast on
AuthenticationError. A bad or missing API key does not resolve itself on retry; surface it loudly rather than looping. - Type your own functions against
MessageandContentBlock, notdictorAny. This is what lets a type checker catch a malformed call site before you run the code, and what makesisinstance()narrowing on content blocks actually useful.
FAQs
Is it safe to skip retry configuration and use the SDK's defaults?
Usually, yes.
The defaults (max_retries=2, timeout=600.0 seconds) are reasonable general-purpose values; only override them when a specific scenario (interactive UI, background batch job) calls for something tighter or looser.
Which item on this list matters most for a new production service?
Constructing the client once at startup (item A1) and choosing sync vs async to match your program's actual concurrency model (item B1) - both are foundational, and getting either wrong compounds into every other item on this list.
Do I need to follow the streaming rules if my responses are always short?
Not strictly - a short, bounded response is unlikely to hit a client-side timeout non-streaming. Streaming becomes important as max_tokens grows or as you want incremental UI feedback.
Why does the async section warn against calling the sync client inside async def?
Because it blocks the entire event loop, not just the current request - every other request being served by that same async process stalls for the duration of the blocking call, not only the one that made it.
Is catching a single broad exception ever acceptable?
For a genuinely disposable script where any failure just means "stop and print an error," a broad catch is fine. In any code that's meant to run unattended or serve traffic, the specific-first chain in section D is worth the extra lines.
What's the most common mistake teams make with this SDK?
Awaiting async tool calls or requests sequentially in a loop instead of using asyncio.gather - it compiles, it works, and it silently gives up the concurrency that was the entire reason to choose the async client.
Should I always add a custom http_client for production deployments?
No.
Add one only when there's a concrete requirement (a proxy, a private CA bundle, connection-pool tuning) - it's an advanced customization point, not a default production hardening step.
Do these practices differ between Anthropic() and AsyncAnthropic()?
The underlying practices (retry handling, streaming discipline, typed responses) are identical; only the syntax (await, async with, async for) differs between the two client classes.
How often should I revisit this checklist?
Whenever you add a new call site with different reliability or latency requirements than your existing ones, or after a production incident that traced back to error handling or timeout configuration.
Is pinning the SDK version really necessary for a small project?
It's lower-stakes for a small project, but still worth doing - an unpinned dependency can introduce a new response field or block type between deploys with no warning, which is a harder bug to trace than a version bump you chose deliberately.
Related
- The Anthropic Python SDK Mental Model - the conceptual foundation behind this checklist
- Choosing Between Anthropic() and AsyncAnthropic() in Production Code - the full sync/async decision, expanded from section B
- Python SDK Retry and Timeout Configuration Reference - every setting behind section A's retry/timeout rule
- Python SDK Exception Types at a Glance - the full exception hierarchy behind section D
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.