Machine Learning course menu
Using LLM APIs in Python: Claude API Beginner Guide
Call a large language model from Python: API keys, the Anthropic SDK, messages and system prompts, structured JSON output for classification and extraction, tool use, streaming, tokens and cost, and evaluating LLM outputs like any other model.
On this page
Large language models (LLMs) can classify, extract, summarise and answer questions about text with no training data at all: you describe the task in plain language. Most applications use them through an API: your code sends a request over the internet and gets the model's answer back.
This lesson uses Anthropic's Claude API and its official Python SDK. Other providers follow the same overall pattern.
Setup#
pip install anthropicCreate an API key in the Claude Console, then store it in an environment variable, never in your code or in Git:
# macOS / Linux
export ANTHROPIC_API_KEY="your-key-here"
# Windows PowerShell
$env:ANTHROPIC_API_KEY = "your-key-here"The SDK reads ANTHROPIC_API_KEY automatically.
Your first request#
import anthropic
client = anthropic.Anthropic() # reads ANTHROPIC_API_KEY from the environment
response = client.messages.create(
model="claude-opus-5-5",
max_tokens=1000,
messages=[{"role": "user", "content": "Explain overfitting in two sentences."}],
)
for block in response.content:
if block.type == "text":
print(block.text)modelchooses which model answers. Model names change over time; check the provider's model list.max_tokenscaps the length of the reply (see tokens below).messagesis the conversation so far. The API remembers nothing between calls, so for a chat you send the whole history each time, alternating"user"and"assistant"messages.- The reply's
contentis a list of blocks; text answers arrive as blocks withtype == "text".
System prompts#
A system prompt sets the model's role and rules for the whole conversation:
import anthropic
client = anthropic.Anthropic()
response = client.messages.create(
model="claude-opus-5-5",
max_tokens=1000,
system="You are a support assistant for an online electronics shop. Answer in at most three sentences. "
"If you do not know something about an order, say so and suggest contacting support.",
messages=[{"role": "user", "content": "My headphones arrived with a broken cable. What can I do?"}],
)
print(next(block.text for block in response.content if block.type == "text"))Clear, specific instructions (who the users are, what to do, the format you want, and what to do when unsure) matter more than clever wording.
Classification with structured output#
For use inside a program, you want answers in a fixed format, not free text. Structured output constrains the reply to match a schema. With Pydantic models, the SDK's messages.parse validates the result for you:
from typing import Literal
import anthropic
from pydantic import BaseModel
class TicketLabel(BaseModel):
team: Literal["billing", "technical", "delivery", "other"]
urgent: bool
summary: str
client = anthropic.Anthropic()
def classify(ticket: str) -> TicketLabel:
response = client.messages.parse(
model="claude-opus-5-5",
max_tokens=1000,
system="Classify customer support tickets for routing. Urgent means the customer cannot use the product or was charged wrongly.",
messages=[{"role": "user", "content": ticket}],
output_format=TicketLabel,
)
return response.parsed_output
label = classify("I was charged twice for order 4471 and need the extra payment back.")
print(label.team, label.urgent, label.summary)Compared with the TF-IDF classifier, this needed no labelled training data. In return each call costs money and takes longer, and the answers still need checking.
Tool use#
Tools let the model ask your code to do things: look up an order, query a database, call another API. You describe each tool with a name, a description and a JSON schema for its inputs. When the model wants to use one, it replies with a tool_use block; your code runs the function and sends the result back:
import anthropic
client = anthropic.Anthropic()
def get_order_status(order_id: str) -> str:
orders = {"4471": "shipped on 21 October, arriving 25 October"} # stand-in for a real database
return orders.get(order_id, "order not found")
tools = [{
"name": "get_order_status",
"description": "Look up the delivery status of an order by its order ID.",
"input_schema": {
"type": "object",
"properties": {"order_id": {"type": "string", "description": "The order number, e.g. 4471"}},
"required": ["order_id"],
},
}]
messages = [{"role": "user", "content": "Where is my order 4471?"}]
while True:
response = client.messages.create(model="claude-opus-5-5", max_tokens=1000, tools=tools, messages=messages)
if response.stop_reason != "tool_use":
break
messages.append({"role": "assistant", "content": response.content})
results = [
{"type": "tool_result", "tool_use_id": block.id, "content": get_order_status(**block.input)}
for block in response.content if block.type == "tool_use"
]
messages.append({"role": "user", "content": results})
print(next(block.text for block in response.content if block.type == "text"))The loop keeps going while the model asks for tools, then prints the final answer. This request, run, reply loop is the basis of AI agents. Your code decides which tools exist, so check inputs and never give a model tools that can do damage without a human approving.
Streaming#
Long answers take time to generate. Streaming shows text as it is produced, which feels much faster in a chat interface:
import anthropic
client = anthropic.Anthropic()
with client.messages.stream(
model="claude-opus-5-5",
max_tokens=4000,
messages=[{"role": "user", "content": "Write a short study plan for learning machine learning in 8 weeks."}],
) as stream:
for text in stream.text_stream:
print(text, end="", flush=True)Tokens and cost#
Models read and write tokens, pieces of words (roughly three to four characters of English text each on average). You pay per token, separately for input and output, and output tokens cost more. Every response reports its usage:
print(response.usage.input_tokens, response.usage.output_tokens)Ways to keep costs down:
- keep prompts focused and set a sensible
max_tokens; - reuse a long, unchanging system prompt with prompt caching (the provider stores it, and repeated reads are much cheaper);
- send large offline jobs through the batch API, which is cheaper when you can wait for results;
- choose the smallest model that meets your quality bar, measured on your own examples.
Check the provider's pricing page for current prices; they change often.
Errors and limits#
Requests can fail: invalid keys, rate limits when you send too many requests, temporary server errors. The SDK retries some failures automatically; catch the rest with its typed exceptions:
import anthropic
client = anthropic.Anthropic()
try:
response = client.messages.create(model="claude-opus-5-5", max_tokens=200,
messages=[{"role": "user", "content": "Hello"}])
except anthropic.RateLimitError:
print("Too many requests: wait and retry later.")
except anthropic.APIStatusError as e:
print("API error", e.status_code)
except anthropic.APIConnectionError:
print("Could not reach the API.")Also check response.stop_reason: "end_turn" means the model finished, "max_tokens" means the reply was cut off, and "refusal" means the model declined the request.
Evaluate LLMs like any other model#
An LLM-powered classifier is still a classifier. Before trusting it:
- Collect a few hundred real examples with correct labels.
- Run the LLM on them and compute accuracy, precision and recall, just as in Classification Metrics.
- Read the mistakes, improve the instructions, and re-measure.
- Compare with a cheap baseline such as TF-IDF + logistic regression. If it is nearly as good, it may be the better choice at scale.
LLMs can state wrong facts confidently. For anything factual, give them the source material (as in RAG) and ask them to answer only from it.
Common mistakes#
Practice#
Practice exercise
Design (on paper or in code) a Pydantic model for extracting details from job adverts: the job title, the city, whether it is remote, the minimum years of experience (which may be missing), and a list of required skills. Which fields should be optional?
Show hint
Use str, bool, list[str] and int | None for a value that may not be stated.
Show solution
from pydantic import BaseModel
class JobAdvert(BaseModel):
title: str
city: str | None # fully remote jobs may have no city
remote: bool
min_years_experience: int | None
skills: list[str]Pass it as output_format=JobAdvert to client.messages.parse, exactly like TicketLabel above. Making missing details None (rather than forcing a value) stops the model inventing numbers that the advert never stated.
Key takeaway#
Next, putting models into production: Saving and Loading Models.
Finished this lesson?