How to Build Your First AI Application with Python (Part 2: Error Handling and Streaming)
Part 16 built a real, working request — but a genuine production application needs to handle real failures gracefully, and stream responses instead of waiting silently. This part adds both to the exact same code.
Real error handling, directly addressing part 15's rate limits
import os
from dotenv import load_dotenv
from anthropic import Anthropic, APIStatusError, APIConnectionError
load_dotenv()
client = Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])
def ask_support_assistant(user_question: str, plan_data: str) -> str:
try:
response = client.messages.create(
model="claude-sonnet-5",
max_tokens=1024,
system=(
"You are a customer support assistant for Bright Leaf "
"Coffee. Only answer using real information provided in "
"this conversation. If you don't have the information "
"needed, say so directly. Keep responses to 2-3 sentences."
),
messages=[
{
"role": "user",
"content": f"{plan_data}\n\nCustomer question: {user_question}",
}
],
)
return response.content[0].text
except APIStatusError as e:
if e.status_code == 429:
return "We're experiencing high demand right now — please try again in a moment."
return "Something went wrong on our end. Please try again shortly."
except APIConnectionError:
return "We couldn't reach our support assistant — please check your connection and try again."This directly implements part 15's documentation-reading checklist in real code — APIStatusError with a real 429 status code is exactly the rate-limit case flagged in that part's documentation-reading guidance, handled with a real, specific, user-facing message rather than crashing or showing a raw technical error to a real customer.
Real error handling here isn't defensive programming for its own sake — it's directly responding to real, documented failure modes covered conceptually in part 15. A real customer support assistant that crashes visibly the moment a rate limit is hit, instead of showing a graceful real message, is a genuinely worse experience than the assistant being briefly unavailable with clear, honest communication about it.
Real streaming: the token-by-token behavior from part 7, in actual code
def ask_support_assistant_streaming(user_question: str, plan_data: str):
with client.messages.stream(
model="claude-sonnet-5",
max_tokens=1024,
system=(
"You are a customer support assistant for Bright Leaf "
"Coffee. Only answer using real information provided in "
"this conversation. Keep responses to 2-3 sentences."
),
messages=[
{
"role": "user",
"content": f"{plan_data}\n\nCustomer question: {user_question}",
}
],
) as stream:
for text in stream.text_stream:
print(text, end="", flush=True)
print()This is exactly part 7's real streaming behavior, implemented directly — rather than waiting for client.messages.create() to return the entire, complete response at once, client.messages.stream() yields each real chunk of generated text as it's produced, exactly matching the token-by-token generation process from part 4.
Why streaming matters for a real, practical reason beyond feel
Without streaming: a real user waits, seeing nothing, for the ENTIRE
response to finish generating — genuinely longer for longer answers
With streaming: real, visible output begins almost immediately,
even though the total time to fully finish is roughly the sameThis directly connects back to part 7's ChatGPT explanation — streaming doesn't make total generation faster, but it dramatically improves real, perceived responsiveness, which is exactly why nearly every real, production chat-based AI product streams responses rather than waiting for completion.
Combining both: real, production-shaped code
def get_support_response(user_question: str, plan_data: str) -> str | None:
try:
full_response = ""
with client.messages.stream(
model="claude-sonnet-5",
max_tokens=1024,
system="...",
messages=[{"role": "user", "content": f"{plan_data}\n\n{user_question}"}],
) as stream:
for text in stream.text_stream:
full_response += text
yield text
except (APIStatusError, APIConnectionError):
yield "We're having trouble responding right now — please try again shortly."This real, combined version — a Python generator, yielding text chunks as they arrive, wrapped in the same real error handling from earlier — is genuinely close to production-shape for a real web application, where a frontend would consume this stream to display text incrementally to an actual user.
Next: building your first AI application, part 3 — adding real conversation memory and managing the context window from part 9 as a conversation grows.