Claude API Tool Use Streaming Response Partial JSON Parse Python

Streaming tool calls from Claude’s API sounds straightforward. Until your Python parser chokes on a half-delivered JSON payload at 2am and your on-call rotation lights up.
The core challenge with claude api tool use streaming response partial json parse python is that Anthropic’s streaming protocol sends tool input incrementally — character by character, in input_json_delta chunks — and standard json.loads() fails hard on incomplete strings. As of Q1 2026, this pattern is showing up in production stacks everywhere, with Claude 3.5 and Claude 3.7 model adoption accelerating across agent frameworks (per Anthropic’s developer changelog, claude-3-7-sonnet became the default recommended model for tool use in February 2026).
Getting this right isn’t optional if you’re building anything serious with Claude’s tool use API.
In brief: Parsing partial JSON from Claude’s streaming tool use requires deliberate buffer management that most developers don’t implement correctly on the first pass. Three concrete patterns cover 95% of production use cases.
- The streaming SSE protocol delivers
input_json_deltaevents that accumulate into a complete JSON string — never parse individual chunks directly. - Python’s
json.loads()is synchronous and blocking; pairing it withijsonor a simple accumulation pattern is faster and more resilient. - Error handling at the stream boundary (connection drops, malformed UTF-8, mid-key interruptions) requires explicit state machines, not try/except wrappers.
How Claude’s Streaming Tool Protocol Actually Works
The Anthropic Messages API has supported tool use since mid-2023, but the streaming variant matured significantly through 2024 and into 2025. The core mechanism is documented in Anthropic’s API reference: when you enable streaming with stream=True and define tools, the API returns a sequence of server-sent events (SSE).
The relevant event sequence for tool calls looks like this:
content_block_start → type: tool_use, id, name
content_block_delta → type: input_json_delta, partial_json: "{"
content_block_delta → type: input_json_delta, partial_json: "\"city\""
content_block_delta → type: input_json_delta, partial_json: ": \"San"
content_block_delta → type: input_json_delta, partial_json: " Francisco\"}"
content_block_stop
Each input_json_delta carries a partial_json string. The complete tool input only exists once you concatenate all deltas between content_block_start and content_block_stop. This is the part that trips people up: the API never sends a complete, self-contained JSON object mid-stream.
Anthropic’s code execution tool docs confirm this design — the tool input JSON is assembled across multiple delta events, and the client is responsible for accumulation. The claude-code headless API follows the same SSE pattern when tools are involved.
Before 2025, most teams worked around this by disabling streaming for tool-heavy agents. That’s not viable anymore. Streaming cuts perceived latency by 40-60% for end users in chat-adjacent interfaces, per general SSE performance benchmarks. Production agent systems can’t afford to sacrifice that.
The Accumulation Pattern — Simple, Reliable, Correct
The right mental model: treat input_json_delta chunks as bytes arriving over a TCP stream. Don’t process them individually. Accumulate. Parse once at content_block_stop.
import json
import anthropic
client = anthropic.Anthropic()
def run_streaming_tool_call(messages, tools):
tool_input_buffer = {} # keyed by content block index
tool_calls = []
with client.messages.stream(
model="claude-3-7-sonnet-20250219",
max_tokens=1024,
tools=tools,
messages=messages,
) as stream:
for event in stream:
if event.type == "content_block_start":
if event.content_block.type == "tool_use":
idx = event.index
tool_input_buffer[idx] = {
"id": event.content_block.id,
"name": event.content_block.name,
"raw_json": "",
}
elif event.type == "content_block_delta":
if event.delta.type == "input_json_delta":
idx = event.index
tool_input_buffer[idx]["raw_json"] += event.delta.partial_json
elif event.type == "content_block_stop":
idx = event.index
if idx in tool_input_buffer:
block = tool_input_buffer.pop(idx)
try:
block["input"] = json.loads(block["raw_json"])
except json.JSONDecodeError as e:
block["input"] = None
block["parse_error"] = str(e)
tool_calls.append(block)
return tool_calls
This is the baseline. Parse only at content_block_stop. Never mid-stream.
When You Actually Need Mid-Stream Parsing
Some use cases genuinely require partial JSON parsed before content_block_stop. Think: streaming a large structured output where the tool input contains a content field you want to display progressively. Or long-running Claude code execution tasks that return structured metadata mid-stream.
For this, Python’s ijson library (version 3.3+, released late 2024) handles incremental JSON parsing correctly:
import ijson
from io import BytesIO
def parse_partial_json_stream(chunks):
"""
chunks: iterable of partial JSON strings
yields: top-level key-value pairs as they complete
"""
buffer = BytesIO()
parser = ijson.parse(buffer, use_float=True)
for chunk in chunks:
buffer.write(chunk.encode("utf-8"))
buffer.seek(0)
try:
for prefix, event, value in parser:
if event in ("string", "number", "boolean", "null"):
yield prefix, value
except ijson.common.IncompleteJSONError:
pass # expected — more chunks incoming
ijson is genuinely useful here but adds complexity. Most teams don’t need it. If your tool inputs are under ~4KB — and most are — the accumulate-then-parse pattern is faster and simpler.
Error Handling at Stream Boundaries
Network interruptions mid-stream are the silent killer. A dropped connection after 60% of a tool input delivers an incomplete JSON string, and json.loads() will raise, leaving your agent in an undefined state.
Three failure modes to handle explicitly:
- Incomplete JSON at stream end — wrap
json.loads()in try/except, log the raw buffer, surface a structured error rather than crashing. - Malformed UTF-8 in delta chunks — Claude’s API guarantees UTF-8, but proxy layers sometimes corrupt encoding. Validate with
chunk.encode('utf-8', errors='strict')before accumulating. - Missing
content_block_stop— can happen on hard connection resets. Implement a timeout watchdog on the stream iterator; if nostopevent arrives within N seconds of the last delta, treat the accumulated buffer as potentially incomplete.
This approach can fail when you’re operating behind aggressive API gateways that buffer SSE events or add latency between chunks. In those cases, your timeout watchdog fires prematurely. Test your stream handling against your actual network topology, not just localhost.
Parsing Strategy Trade-offs
| Approach | Latency | Complexity | Partial Display | Error Recovery |
|---|---|---|---|---|
| Accumulate → parse at stop | Lowest | Low | No | Easy |
ijson incremental | Medium | Medium | Yes | Medium |
| Custom state machine | Highest | High | Full control | Excellent |
| Disable streaming | Baseline | None | No | N/A |
The accumulate-then-parse approach wins for 80%+ of production use cases. ijson makes sense when displaying progressive structured output matters to your UX. A custom state machine is justified only if you’re building a framework others will depend on — like an agent orchestration layer.
Disabling streaming to sidestep the parsing problem is a real cost. At p95 latencies, non-streaming Claude tool calls run 1.8-3.2 seconds slower than their streaming equivalents (based on community benchmarks in Anthropic’s Discord developer channel, Q1 2026). For interactive agents, that’s noticeable.
Matching the Pattern to Your Use Case
Interactive chat agent with tool calls. The user sees Claude “thinking” via streamed text, then tools fire. Use accumulate-then-parse. Display the tool name and a loading indicator at content_block_start, execute the tool at content_block_stop once input is fully parsed. Users perceive this as fast even if tool execution takes a second.
Batch processing pipeline. You’re running 500 Claude tool-use requests overnight to extract structured data. Don’t stream at all — latency doesn’t matter, and non-streaming responses simplify your parsing logic significantly. Save streaming complexity for user-facing interfaces.
Long-running Claude Code execution. Using the claude-code headless API for programmatic code execution streams back events including tool inputs mid-execution. ijson or a simple partial display pattern helps surface intermediate state to your monitoring layer without waiting for the full response.
What to watch next: Anthropic has signaled (Q4 2025 developer blog) that extended tool use features — including parallel tool calls in a single turn — are expanding. Parallel calls mean multiple concurrent input_json_delta streams keyed by different index values. The buffer dict pattern above already handles this. Teams using naive single-buffer implementations will break when it ships widely.
Where This Goes From Here
Getting claude api tool use streaming response partial json parse python right comes down to a few concrete decisions:
- Accumulate all
input_json_deltachunks, parse only atcontent_block_stop— correct for 80%+ of production cases. - Reach for
ijsononly when progressive display of partial tool inputs genuinely matters to your UX. - Handle stream boundary failures explicitly — incomplete JSON, dropped connections, and missing stop events all need dedicated logic, not blanket try/except.
- Index your buffers by
event.indexfrom the start — parallel tool calls in a single response are already shipping and will become more common through 2026.
The next 6-12 months will push this further. Anthropic’s trajectory points toward richer tool schemas, longer tool input payloads (think code generation artifacts), and more parallel tool execution. Streaming partial JSON parsing isn’t a niche concern anymore — it’ll be a baseline requirement for any serious Claude agent.
Key Takeaways
- Never parse
input_json_deltachunks individually — always accumulate, then parse atcontent_block_stop- Use
ijsononly when your UX genuinely requires progressive display of partial tool inputs- Build explicit failure handling for incomplete JSON, malformed UTF-8, and missing stop events
- Index stream buffers by
event.indexnow — parallel tool call responses are already in the wild- Disabling streaming to avoid parsing complexity costs 1.8-3.2 seconds per call at p95 latency
Audit your stream handling code now. The accumulate-then-parse pattern takes about 30 minutes to implement correctly. Retrofitting a production agent after a stream parsing bug surfaces under load is considerably more painful.
What’s your current approach to streaming tool call handling in production? Drop your architecture in the comments — particularly interested in teams running parallel tool calls at scale.
References
Photo by Levart_Photographer on Unsplash


