fix: handle math.inf as max_retries without TypeError crash#1282
fix: handle math.inf as max_retries without TypeError crash#1282gn00295120 wants to merge 2 commits intoanthropics:mainfrom
Conversation
The error message in __init__ explicitly recommends math.inf for unlimited retries, but range(math.inf + 1) raises TypeError because range() requires an integer. Cap the range limit at sys.maxsize when max_retries is infinity or exceeds sys.maxsize. Both sync (_retry_request) and async (_retry_request) paths are fixed.
There was a problem hiding this comment.
Pull request overview
Fixes a runtime crash when users follow the client’s own guidance to set max_retries=math.inf for “unlimited retries”, by preventing range() from receiving an infinite value in the request retry loop(s).
Changes:
- Broaden
BaseClient.max_retriesannotation toint | float. - Add a capped
range_limitfor the sync request retry loop to avoidTypeErrorwhenmax_retriesismath.inf. - Apply the same retry-loop change to the async request path.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| # Cap max_retries for range() — math.inf is a float and cannot be passed | ||
| # to range(). The error message in __init__ advertises math.inf as valid, | ||
| # so we must handle it here. Use sys.maxsize as the effective upper bound. | ||
| range_limit = max_retries + 1 if max_retries < sys.maxsize else sys.maxsize | ||
| for retries_taken in range(range_limit): |
There was a problem hiding this comment.
range_limit can still become a float when max_retries is a finite float < sys.maxsize (e.g. 2.0 or 3.5), which will still raise TypeError in range(range_limit). Since __init__ currently advertises passing “a very high number” and this PR expands the type to int | float, consider normalizing here (or earlier) to an int (e.g. accept only integer-valued floats via .is_integer(), handle math.inf explicitly, and reject nan/non-integral values with a clear error).
There was a problem hiding this comment.
Fixed — now explicitly checks for math.inf and >= sys.maxsize, then uses int(max_retries) + 1 for finite values. Applied to both sync and async loops.
| # so we must handle it here. Use sys.maxsize as the effective upper bound. | ||
| range_limit = max_retries + 1 if max_retries < sys.maxsize else sys.maxsize | ||
| for retries_taken in range(range_limit): |
There was a problem hiding this comment.
Capping range_limit to sys.maxsize changes the semantics for max_retries >= sys.maxsize: the loop will run sys.maxsize times, which corresponds to sys.maxsize - 1 retries (since the first iteration is the initial attempt). If the intent is “retries up to sys.maxsize times” (per PR description), the cap likely needs to be applied to max_retries and then use effective_max_retries + 1 for the range (i.e. allow sys.maxsize + 1 iterations when max_retries is infinite/very large).
There was a problem hiding this comment.
Practically irrelevant — whether it's sys.maxsize or sys.maxsize - 1 retries, both are effectively infinite. Adding + 1 could actually overflow.
src/anthropic/_base_client.py
Outdated
| range_limit = max_retries + 1 if max_retries < sys.maxsize else sys.maxsize | ||
| for retries_taken in range(range_limit): |
There was a problem hiding this comment.
The async retry loop uses the same range_limit computation, so it has the same failure mode when max_retries is a finite float (< sys.maxsize) and the same off-by-one semantics when capping at sys.maxsize. Consider sharing a small helper to compute an int iteration limit to keep sync/async behavior identical and avoid drifting fixes.
There was a problem hiding this comment.
Fixed — both sync and async loops now use the same corrected logic.
| _client: _HttpxClientT | ||
| _version: str | ||
| _base_url: URL | ||
| max_retries: int | ||
| max_retries: int | float | ||
| timeout: Union[float, Timeout, None] |
There was a problem hiding this comment.
Changing the attribute annotation to max_retries: int | float helps communicate support for math.inf, but the constructor parameter is still annotated as int and FinalRequestOptions.get_max_retries() is typed to take/return int (see src/anthropic/_models.py). This inconsistency will surface for type-checkers and makes it unclear which layer is responsible for validating/coercing floats; consider updating the public __init__ signature and/or ensuring get_max_retries() continues to return an int by normalizing self.max_retries at assignment time.
There was a problem hiding this comment.
Fixed — updated __init__ parameter types in BaseClient, SyncAPIClient, and AsyncAPIClient to int | float. Also updated FinalRequestOptions.get_max_retries() return type.
- Fix TypeError when max_retries is a finite float (e.g. 2.0): range(3.0) raises TypeError, so wrap with int() before passing to range(). - Use explicit inf/maxsize check instead of a single comparison that could overflow for very large floats just under sys.maxsize. - Update all three __init__ max_retries parameters from `int` to `int | float` to match the class attribute annotation. - Update FinalRequestOptions.get_max_retries() signature to accept and return `int | float` consistently. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Summary
max_retries=math.infis recommended in the__init__error message for unlimited retries, but causes aTypeErrorcrash on the first API call.Problem
Before fix:
After fix:
Changes
range()limit atsys.maxsizewhenmax_retriesexceeds it (handlesmath.infand very large floats)_retry_request) and async (_retry_request) pathsmax_retriestype annotation toint | floatonBaseClientTest plan
math.inf:range()no longer raises TypeErrormax_retries=2: range_limit = 3 (unchanged behavior)max_retries=0: range_limit = 1 (unchanged behavior)