Send Your First Message
The Goal
Everything so far has prepared for this step. You have:
- the SDK installed (Lesson 4),
- an API key set (Lessons 6–8), and
- a client created (Lesson 10).
Now you will send a message to Claude, receive the reply, and understand every part of the exchange. This lesson builds the request step by step, explains what happens behind the scenes, and shows how to extract the answer correctly from the response.
The Minimal Working Program
Create a file named hello_claude.py:
from anthropic import Anthropic
client = Anthropic()
message = client.messages.create(
model="claude-opus-5-5",
max_tokens=1024,
messages=[
{"role": "user", "content": "Hello, Claude! In one sentence, what can you help me with?"}
],
)
for block in message.content:
if block.type == "text":
print(block.text)
Run it:
python hello_claude.py
Example output (Claude's wording will vary):
I can help you write and debug code, explain concepts, analyze documents, draft and edit text, and work through problems step by step.
If you see an answer, your setup is complete and working end to end.
Line-by-Line Explanation
1. Import the Client Class
from anthropic import Anthropic
This makes the Anthropic class available (Lesson 9).
2. Create the Client
client = Anthropic()
The client reads your key from ANTHROPIC_API_KEY and applies the default timeout and retry settings (Lesson 10). No network request happens yet.
3. Call messages.create()
message = client.messages.create(
This is the SDK method for the Messages API endpoint, POST /v1/messages. It sends one request and waits until the complete reply has arrived. The return value is a Message object.
4. Choose the Model
model="claude-opus-5-5",
The model ID selects which Claude model answers. claude-opus-5-5 is a strong, general-purpose default. Lesson 12 explains how to choose between models.
5. Set the Output Limit
max_tokens=1024,
max_tokens is the maximum number of tokens Claude may generate. It is required. Claude usually stops well before the limit when the answer is complete. Lesson 13 explains how to pick a value, and why very small values can cause problems.
6. Provide the Conversation
messages=[
{"role": "user", "content": "Hello, Claude! In one sentence, what can you help me with?"}
],
messages is a list of conversation turns. Each turn is a dictionary with:
role: who is speaking."user"is you;"assistant"is Claude.content: what was said.
Your first request contains one user turn. Later lessons add earlier turns to create multi-turn conversations (Lessons 23–28).
Why it is a list even for one message: the API is stateless (Lesson 2). Every request must include the whole conversation Claude should consider. A list represents one turn and fifty turns in the same way.
7. Read the Answer
for block in message.content:
if block.type == "text":
print(block.text)
message.content is a list of content blocks. The loop prints the text of every text block and skips any other kind of block. The next section explains why this matters.
Why You Loop Over Content Blocks
It is tempting to write:
print(message.content[0].text)
This assumes the first block is always text. With current models, that assumption is not safe.
Claude Opus 5.5 uses adaptive thinking, which is always on. Before answering, the model may reason internally, and that reasoning can appear in the response as a thinking block placed before the text block. By default the thinking text itself is omitted, but the block can still be there.
You can see this by printing the block types:
print([block.type for block in message.content])
Depending on how much the model decided to think, the output may be:
['thinking', 'text']
or simply:
['text']
If the first block is a thinking block, message.content[0].text raises:
AttributeError: 'ThinkingBlock' object has no attribute 'text'
The loop handles both cases correctly. That is why every example in this tutorial reads text this way.
A Reusable Helper
Most programs need the reply as a single string. Collect all the text blocks:
from anthropic.types import Message
def get_text(message: Message) -> str:
"""Return the combined text of all text blocks in a response."""
return "".join(block.text for block in message.content if block.type == "text")
answer = get_text(message)
print(answer)
The generator expression visits each block, keeps only those whose type is "text", and "".join(...) combines their text into one string. If there are no text blocks, the result is an empty string rather than an exception.
Note: Lesson 16 (Understand Message Blocks) covers all block types, including thinking blocks, tool calls, and others, and how to handle each one.
What Happens When You Call create()
messages.create(model=..., max_tokens=..., messages=[...])
│
▼
1. SDK validates and serializes the arguments into JSON
2. SDK adds headers: Authorization, anthropic-version, content-type
3. HTTPS POST to https://api.anthropic.com/v1/messages
│
▼
4. API authenticates the key and validates the request
5. The model processes the input (it may think first) and generates the reply
6. API returns JSON: id, content blocks, stop_reason, usage, ...
│
▼
7. SDK converts the JSON into a typed Message object
8. Your code reads message.content
If step 3 fails because of the network, or step 4 or 5 returns a retryable error, the SDK retries automatically. Otherwise, it raises an exception (Lesson 1).
Inspecting the Whole Response
To see everything the API returned, convert the response to JSON:
print(message.to_json())
Example output (shortened; your values will differ):
{
"id": "msg_01XFDUDYJgAACzvnptvVoYEL",
"type": "message",
"role": "assistant",
"model": "claude-opus-5-5",
"content": [
{
"type": "text",
"text": "I can help you write and debug code, explain concepts, ..."
}
],
"stop_reason": "end_turn",
"stop_sequence": null,
"usage": {
"input_tokens": 24,
"output_tokens": 41
}
}
The fields you will use most:
| Field | Access in Python | Meaning |
|---|---|---|
id | message.id | Unique ID of this response |
model | message.model | The model that answered |
content | message.content | List of content blocks |
stop_reason | message.stop_reason | Why generation ended (Lesson 20) |
usage | message.usage.input_tokens, message.usage.output_tokens | Tokens used, which determine cost |
| Request ID | message._request_id | ID for support and debugging |
Check Why Claude Stopped
Always look at stop_reason before trusting the answer:
stop_reason | Meaning | What to do |
|---|---|---|
end_turn | Claude finished naturally | Use the answer |
max_tokens | The max_tokens limit was reached | The answer is incomplete; raise the limit (Lesson 13) |
refusal | Claude declined the request | Do not treat the content as a normal answer |
Other values, such as stop_sequence and tool_use, appear when you use the related features (Lessons 20, 43, and 85).
Practical Example: A Command-Line Question Tool
This script turns your first message into a small, reusable tool. You pass a question on the command line and get the answer, with optional details and proper error handling.
Save it as ask.py:
"""ask.py: ask Claude a question from the command line.
Usage:
python ask.py "What is a Python decorator?"
python ask.py --details "Explain list comprehensions briefly."
"""
import argparse
import sys
import anthropic
from anthropic import Anthropic
from anthropic.types import Message
MODEL = "claude-opus-5-5"
def get_text(message: Message) -> str:
return "".join(block.text for block in message.content if block.type == "text")
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Ask Claude a question.")
parser.add_argument("question", help="The question to ask")
parser.add_argument("--details", action="store_true", help="Show response metadata")
parser.add_argument("--max-tokens", type=int, default=2048, help="Output token limit")
return parser.parse_args()
def main() -> int:
args = parse_args()
client = Anthropic()
try:
message = client.messages.create(
model=MODEL,
max_tokens=args.max_tokens,
messages=[{"role": "user", "content": args.question}],
)
except anthropic.AuthenticationError:
print("Error: the API key is missing or invalid.", file=sys.stderr)
return 1
except anthropic.APIConnectionError:
print("Error: could not connect to the Claude API.", file=sys.stderr)
return 1
except anthropic.APIStatusError as error:
print(f"Error {error.status_code}: {error.message}", file=sys.stderr)
return 1
if message.stop_reason == "refusal":
print("Claude declined to answer this request.", file=sys.stderr)
return 2
print(get_text(message))
if message.stop_reason == "max_tokens":
print("\n[Warning] The answer was cut off. Try a larger --max-tokens.", file=sys.stderr)
if args.details:
print("\n---")
print(f"Model: {message.model}")
print(f"Blocks: {[block.type for block in message.content]}")
print(f"Stop reason: {message.stop_reason}")
print(f"Input tokens: {message.usage.input_tokens}")
print(f"Output tokens: {message.usage.output_tokens}")
print(f"Request ID: {message._request_id}")
return 0
if __name__ == "__main__":
sys.exit(main())
Run it:
python ask.py --details "What is a Python decorator? Answer in two sentences."
Example output:
A Python decorator is a function that takes another function and returns a modified version of it, letting you add behavior such as logging or caching without changing the original code. You apply it by writing @decorator_name above a function definition.
---
Model: claude-opus-5-5
Blocks: ['thinking', 'text']
Stop reason: end_turn
Input tokens: 21
Output tokens: 96
Request ID: req_018EeWyXxfu5pfWkrYcMdjWG
How the Script Works
argparsereads the question and options from the command line.--detailsis a flag (store_true), and--max-tokensis converted to an integer automatically.python ask.py --helpprints usage information for free.- The request uses the question exactly as typed, as a single user turn.
- The error handlers separate the three most common failure types, a key problem, a network problem, and any other API error, and print to
sys.stderr. Error messages go to the error stream, so redirecting the answer to a file (python ask.py "..." > answer.txt) captures only the answer. - The refusal check stops early with a distinct exit code (
2) if Claude declined, so scripts callingask.pycan tell a refusal apart from an error. get_text()returns the combined text, skipping any thinking block.- The
max_tokenswarning tells the user when the answer is incomplete instead of silently printing a truncated answer. --detailsshows the metadata. Notice thatoutput_tokenscan be noticeably larger than the visible answer. Tokens spent on thinking are counted, and billed, as output tokens even when the thinking text is not shown.
Variations
A Multi-Line Prompt
Triple-quoted strings make longer prompts readable:
prompt = """Review this Python function and suggest one improvement:
def total(items):
t = 0
for i in items:
t = t + i
return t
"""
message = client.messages.create(
model="claude-opus-5-5",
max_tokens=2048,
messages=[{"role": "user", "content": prompt}],
)
print(get_text(message))
Including Data in the Prompt
Use f-strings to insert data, such as user input or file contents, into the prompt:
from pathlib import Path
notes = Path("meeting_notes.txt").read_text(encoding="utf-8")
message = client.messages.create(
model="claude-opus-5-5",
max_tokens=2048,
messages=[{"role": "user", "content": f"List the action items in these notes:\n\n{notes}"}],
)
print(get_text(message))
Warning: When a prompt includes text from users or external files, that text can contain instructions that try to change Claude's behavior. This is called prompt injection. Lessons 40 and 175 explain how to reduce that risk.
An Interactive Loop
A simple loop lets you ask several independent questions:
while True:
question = input("You: ").strip()
if question.lower() in {"quit", "exit"}:
break
if not question:
continue
reply = client.messages.create(
model="claude-opus-5-5",
max_tokens=2048,
messages=[{"role": "user", "content": question}],
)
print(f"Claude: {get_text(reply)}\n")
Each question is sent on its own, so Claude does not remember earlier questions in this loop. Turning it into a real conversation, by sending the history with each request, is the subject of Lessons 24–26.
Common Mistakes
Reading message.content[0].text
What happens: AttributeError: 'ThinkingBlock' object has no attribute 'text' appears intermittently, only when the model decided to think before answering.
Fix: loop over the blocks, or use the get_text() helper.
Printing message.content Directly
What happens: you see something like [TextBlock(text='...', type='text', ...)] instead of the plain answer.
Fix: print the extracted text.
Wrong Shape for messages
messages="Hello" # Wrong: must be a list
messages={"role": "user", "content": "Hi"} # Wrong: a dict, not a list of dicts
messages=[{"role": "human", "content": "Hi"}] # Wrong: the role must be "user" or "assistant"
What happens: a BadRequestError (400), or a type error reported by your editor.
Fix: messages=[{"role": "user", "content": "Hi"}].
Forgetting a Required Parameter
What happens: leaving out model, max_tokens, or messages raises an error before or during the request.
Fix: always provide all three.
Setting max_tokens Very Low
What happens: with max_tokens=20, a thinking model can use the whole budget for thinking. The response then has stop_reason == "max_tokens" and no text at all.
Fix: use a comfortable limit, such as 1,024 or more, for normal answers. Lesson 13 explains how to size it.
Ignoring stop_reason
What happens: truncated answers are shown or stored as if they were complete.
Fix: check for max_tokens and refusal before using the text.
Best Practices
- Read text through a helper like
get_text(), never by position. - Check
stop_reasonon every response that matters. - Use generous
max_tokensvalues for open-ended answers. - Handle errors explicitly: at least authentication, connection, and generic API errors.
- Keep the model ID in one place, such as a constant or configuration, so changing models is a one-line change.
- Log the request ID and token usage in real applications, for debugging and cost tracking.
Exercise
- Run
hello_claude.pyand print[block.type for block in message.content]. Run it several times with different questions. Do you always see a thinking block? - Add the
get_text()helper and print the response'sstop_reason,usage.input_tokens, andusage.output_tokens. - Run
ask.pywith--max-tokens 50on a question that needs a long answer, such as "Explain how HTTPS works in detail." What happens? - Modify the interactive loop so that typing
/detailsbefore a question (for example,/details What is JSON?) also prints the token usage.
Step 3: the response stops at the limit. stop_reason is max_tokens, and ask.py prints the warning. The answer is cut off. If the model spent the budget on thinking, there may be no text at all.
Step 4:
while True:
question = input("You: ").strip()
if question.lower() in {"quit", "exit"}:
break
show_details = question.startswith("/details ")
if show_details:
question = question.removeprefix("/details ").strip()
if not question:
continue
reply = client.messages.create(
model="claude-opus-5-5",
max_tokens=2048,
messages=[{"role": "user", "content": question}],
)
print(f"Claude: {get_text(reply)}")
if show_details:
print(f" [tokens in={reply.usage.input_tokens} out={reply.usage.output_tokens}]")
print()
str.removeprefix() (Python 3.9+) removes the command prefix only when it is present, and show_details remembers whether it was there.