Send Your First Message

Su Suriya Ravichandran Updated 10 Nov 2026
11 min read ·Lesson 11 of 200

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:

FieldAccess in PythonMeaning
idmessage.idUnique ID of this response
modelmessage.modelThe model that answered
contentmessage.contentList of content blocks
stop_reasonmessage.stop_reasonWhy generation ended (Lesson 20)
usagemessage.usage.input_tokens, message.usage.output_tokensTokens used, which determine cost
Request IDmessage._request_idID for support and debugging

Check Why Claude Stopped

Always look at stop_reason before trusting the answer:

stop_reasonMeaningWhat to do
end_turnClaude finished naturallyUse the answer
max_tokensThe max_tokens limit was reachedThe answer is incomplete; raise the limit (Lesson 13)
refusalClaude declined the requestDo 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

  1. argparse reads the question and options from the command line. --details is a flag (store_true), and --max-tokens is converted to an integer automatically. python ask.py --help prints usage information for free.
  2. The request uses the question exactly as typed, as a single user turn.
  3. 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.
  4. The refusal check stops early with a distinct exit code (2) if Claude declined, so scripts calling ask.py can tell a refusal apart from an error.
  5. get_text() returns the combined text, skipping any thinking block.
  6. The max_tokens warning tells the user when the answer is incomplete instead of silently printing a truncated answer.
  7. --details shows the metadata. Notice that output_tokens can 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_reason on every response that matters.
  • Use generous max_tokens values 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

  1. Run hello_claude.py and print [block.type for block in message.content]. Run it several times with different questions. Do you always see a thinking block?
  2. Add the get_text() helper and print the response's stop_reason, usage.input_tokens, and usage.output_tokens.
  3. Run ask.py with --max-tokens 50 on a question that needs a long answer, such as "Explain how HTTPS works in detail." What happens?
  4. Modify the interactive loop so that typing /details before a question (for example, /details What is JSON?) also prints the token usage.
Show answers for steps 3 and 4

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.

0 Comments

Reviewed before they appear

No comments yet.

Anthropic SDK Using Python
Anthropic SDK Intro Claude API Intro SDK Requirements Install Anthropic SDK Upgrade the SDK Create an API Key Set API Keys Use Environment Variables Import Anthropic Create a Client Send Your First Message Choose a Claude Model Set Max Tokens Read a Response Print Response Text Understand Message Blocks Use System Prompts Send User Messages Set Temperature Handle Stop Reasons Messages API Intro Create Messages Use Message Roles Send Conversation History Continue a Conversation Build a Chat Loop Reset Chat History Use Multiple Turns Inspect Message IDs Read Usage Data Write Clear Prompts Set a System Prompt Add Context to Prompts Use Prompt Templates Add Examples to Prompts Request JSON Output Use XML Tags Control Response Length Improve Prompt Quality Avoid Prompt Injection Read Text Blocks Read Content Blocks Check Stop Reasons Count Input Tokens Count Output Tokens Count Tokens Before Sending Limit Output Tokens Use Stop Sequences Read Request IDs Parse API Responses Streaming Intro Stream Text Output Use Stream Events Use Stream Helpers Get Final Messages Stream Async Responses Handle Stream Errors Build a Live Chat Stream Long Responses Close Streams Safely Build a Chatbot Keep Chat History Manage Chat Sessions Add Chat Memory Build a CLI Chatbot Build a Web Chatbot Use Flask with Claude Use FastAPI with Claude Return JSON from an API Add Chat Error Messages Async SDK Intro Create an Async Client Send Async Messages Use asyncio Run Parallel Requests Stream Async Text Handle Async Errors Set Async Timeouts Close Async Clients Choose Sync or Async Tool Use Intro Define a Tool Create Tool Schemas Send Tools to Claude Read Tool Calls Run Python Tools Return Tool Results Continue After Tool Use Control Tool Choice Handle Tool Errors Use the beta_tool Decorator Create a Tool Runner Add Tool Docstrings Validate Tool Inputs Use Multiple Tools Limit Tool Calls Log Tool Activity Build a Weather Tool Build a Search Tool Build a Calculator Tool Structured Output Intro Request JSON Data Validate JSON Output Use Pydantic Models Handle Invalid JSON Extract Fields from Text Generate Typed Data Use Image URLs Send Base64 Images Read PDFs with Claude Use Output Schemas Build Data Extraction Test Structured Responses Send Image Inputs Upload Files Use File IDs Process Text Files Summarize Documents Extract Table Data Handle File Errors Read Cache Usage Reduce Repeated Input Costs Debug Cache Misses Prompt Caching Intro Enable Prompt Caching Cache System Prompts Cache Long Context Test Cache Performance Use Cache Breakpoints Cache Best Practices Message Batches Intro Create a Message Batch Set Batch IDs Check Batch Status List Message Batches Get Batch Results Process Batch Outputs Handle Failed Batch Items Use Batches for Bulk Tasks Compare Batches and Requests Handle API Errors Handle Rate Limits Retry Failed Requests Set Request Timeouts Handle Network Errors Use Exponential Backoff Build Reliable API Calls Use Extended Thinking Log API Errors Track Request IDs Paginate API Results Set Thinking Budgets Read Thinking Blocks Use Tool Search Use Web Search Tools Use Computer Tools Use Citations Use Token Counting Read Raw Responses Set Custom Headers Use Claude on AWS Bedrock Use Claude on Vertex AI Use Claude on Microsoft Foundry Configure Cloud Credentials Choose a Cloud Provider Compare API Integrations Configure Provider Clients Handle Cloud Errors Use Cloud SDK Extras Deploy Cloud Apps Protect API Keys Use a .env File Ignore Secrets in Git Validate User Input Reduce Prompt Injection Limit Tool Permissions Avoid Unsafe Tool Calls Sanitize Tool Results Set Usage Limits Review Security Logs Test SDK Functions Mock API Responses Test Error Handling Test Streaming Code Measure Token Usage Track API Costs Configure Logging Deploy with Docker Deploy a FastAPI App Monitor Production Apps Build a Claude Chatbot Build a Text Summarizer Build a Code Explainer Build a JSON Extractor Build a PDF Assistant Build a Streaming Chat App Build a Tool-Calling Agent Build a Batch Processor Build a Research Assistant Build a Claude API Dashboard
Ask about this post
AI Ask about this post

Ask questions about Send Your First Message and get answers drawn from it.

Signed-in readers only.