Create Messages

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

What "Creating a Message" Means

In the Messages API, every call to client.messages.create() creates a new assistant message: Claude's next turn in the conversation you send. The method name reflects the REST convention: POST /v1/messages creates a new message resource.

message = client.messages.create(
    model="claude-opus-5-5",
    max_tokens=1024,
    messages=[{"role": "user", "content": "Suggest a name for a Python study group."}],
)

Lesson 11 used create() for a single, hard-coded request. Real applications create messages programmatically: from templates, user input, files, and database records, often many at a time. This lesson covers how to build requests cleanly, pass optional parameters correctly, use per-request options, and create many messages reliably.


How create() Accepts Arguments

Keyword Arguments Only

All parameters of create() are keyword-only. You must name each one:

# Correct
client.messages.create(model="claude-opus-5-5", max_tokens=1024, messages=messages)

# Wrong: positional arguments raise TypeError
client.messages.create("claude-opus-5-5", 1024, messages)

Why the SDK requires this: create() accepts many parameters. Requiring names makes calls self-documenting and prevents silent mistakes such as swapping two values. It also lets the SDK add new parameters in future versions without breaking existing code.

Building Requests as Dictionaries

Because arguments are keywords, you can build a request as a dictionary and unpack it with **:

request = {
    "model": "claude-opus-5-5",
    "max_tokens": 1024,
    "messages": [{"role": "user", "content": "Suggest a name for a Python study group."}],
}

message = client.messages.create(**request)

**request expands the dictionary into model=..., max_tokens=..., messages=.... This is the most flexible way to create messages in code, because a dictionary can be:

  • built step by step,
  • modified per request,
  • stored, logged, or reused, and
  • passed to related methods such as count_tokens(), after removing fields that method does not accept, such as max_tokens.

Optional Parameters: Omit Them, Do Not Pass None

When a parameter is not needed, leave it out. Do not pass None:

# Risky: None may be sent as null or rejected as an invalid type
client.messages.create(
    model="claude-opus-5-5",
    max_tokens=1024,
    system=None,
    messages=messages,
)

The SDK's type definitions do not accept None for most optional parameters, and the API expects a field to be absent rather than null. The clean pattern is to add optional fields only when they have a value:

def build_request(prompt: str, system: str | None = None, stop: list[str] | None = None) -> dict:
    request = {
        "model": "claude-opus-5-5",
        "max_tokens": 1024,
        "messages": [{"role": "user", "content": prompt}],
    }
    if system:
        request["system"] = system
    if stop:
        request["stop_sequences"] = stop
    return request


message = client.messages.create(**build_request("Hi!", system="Be brief."))

How it works: the dictionary starts with the required fields. Each optional field is added only when the caller provides a value. The request therefore contains exactly what is needed, nothing more.


Per-Request Options

Besides API parameters, create() accepts options that control how a specific request is sent:

message = client.messages.create(
    model="claude-opus-5-5",
    max_tokens=1024,
    messages=[{"role": "user", "content": "Classify: 'Refund please!'"}],
    timeout=20.0,                                          # Seconds, for this request only
    extra_headers={"anthropic-workspace-id": "wrkspc_..."},  # Extra HTTP headers
)
OptionPurpose
timeoutOverrides the client's timeout for this request
extra_headersAdds HTTP headers to this request
extra_queryAdds URL query parameters
extra_bodyAdds raw JSON fields to the request body

extra_body is an escape hatch for new API fields that your SDK version does not yet define. Use it carefully: the SDK cannot validate fields it does not know about.

To change retries or timeouts for several calls, use client.with_options(...) instead (Lesson 10).


Creating Messages from Templates

Most applications send the same kind of request repeatedly with different data. Separate the fixed part, the template, from the variable part, the data:

SUMMARY_SYSTEM = "You summarize customer reviews for a product team. Be neutral and concise."

SUMMARY_TEMPLATE = """<review>
{review}
</review>

Summarize this review in one sentence and label its sentiment as positive, negative, or mixed.
Format: <sentiment> | <summary>"""


def summary_request(review: str) -> dict:
    return {
        "model": "claude-opus-5-5",
        "max_tokens": 1024,
        "system": SUMMARY_SYSTEM,
        "messages": [{"role": "user", "content": SUMMARY_TEMPLATE.format(review=review)}],
    }

Why this structure helps:

  • The instructions live in one place. Improving the prompt improves every request.
  • The data is wrapped in tags, separate from the instructions (Lesson 18).
  • The function returns a fresh dictionary each time, so requests never share mutable state (see Common Mistakes).

Lesson 34 (Use Prompt Templates) goes deeper into template design.


Creating Many Messages

When you process a list of items, each item becomes its own request. Three rules make this reliable:

  1. Reuse one client for all requests (Lesson 10).
  2. Isolate errors per item, so one failure does not stop the whole run.
  3. Collect results and usage, so you know what succeeded and what it cost.
import anthropic

results = []
for item_id, review in reviews:
    try:
        message = client.messages.create(**summary_request(review))
    except anthropic.APIError as error:
        results.append({"id": item_id, "ok": False, "error": str(error)})
        continue
    results.append({"id": item_id, "ok": True, "message": message})

The try block is inside the loop, so an error affects only the current item. continue moves on to the next one.

For large volumes, consider:

  • Asynchronous requests (Lessons 71–80) to process several items at the same time, and
  • Message Batches (Lessons 131–140) for non-urgent bulk work at reduced cost.

Practical Example: A Review Summarizer

This script creates one message per review, using a template, per-item error handling, response checks, and a usage report.

"""summarize_reviews.py: create one message per review and report the results."""

from dataclasses import dataclass

import anthropic
from anthropic import Anthropic
from anthropic.types import Message

MODEL = "claude-opus-5-5"

SYSTEM = "You summarize customer reviews for a product team. Be neutral and concise."

TEMPLATE = """<review>
{review}
</review>

Summarize this review in one sentence and label its sentiment as positive, negative, or mixed.
Reply in exactly this format: <sentiment> | <summary>"""

REVIEWS = [
    ("r1", "Setup took five minutes and the lessons are clear. Loved the quizzes!"),
    ("r2", "Videos are great, but the app crashes whenever I open the certificate page."),
    ("r3", "Too expensive for what you get. I cancelled after a week."),
]


@dataclass
class Outcome:
    review_id: str
    ok: bool
    text: str
    output_tokens: int = 0


def get_text(message: Message) -> str:
    return "".join(block.text for block in message.content if block.type == "text").strip()


def build_request(review: str) -> dict:
    return {
        "model": MODEL,
        "max_tokens": 1024,
        "system": SYSTEM,
        "messages": [{"role": "user", "content": TEMPLATE.format(review=review)}],
    }


def summarize(client: Anthropic, review_id: str, review: str) -> Outcome:
    try:
        message = client.messages.create(**build_request(review))
    except anthropic.APIError as error:
        return Outcome(review_id, False, f"API error: {error}")

    if message.stop_reason != "end_turn":
        return Outcome(review_id, False, f"Unexpected stop_reason: {message.stop_reason}",
                       message.usage.output_tokens)

    return Outcome(review_id, True, get_text(message), message.usage.output_tokens)


def main() -> None:
    client = Anthropic()
    outcomes = [summarize(client, review_id, text) for review_id, text in REVIEWS]

    for outcome in outcomes:
        marker = "OK " if outcome.ok else "ERR"
        print(f"[{marker}] {outcome.review_id}: {outcome.text}")

    succeeded = sum(outcome.ok for outcome in outcomes)
    total_output = sum(outcome.output_tokens for outcome in outcomes)
    print(f"\n{succeeded}/{len(outcomes)} succeeded, {total_output} output tokens in total")


if __name__ == "__main__":
    main()

Example output (wording varies):

[OK ] r1: positive | The reviewer found setup quick and praised the clear lessons and quizzes.
[OK ] r2: mixed | The reviewer likes the videos but reports the app crashes on the certificate page.
[OK ] r3: negative | The reviewer felt the product was overpriced and cancelled after a week.

3/3 succeeded, 412 output tokens in total

How It Works

  1. build_request() returns a fresh request dictionary for each review, combining the fixed system prompt and template with the variable review text.
  2. summarize() isolates everything that can go wrong for one review. API errors and unexpected stop reasons, such as max_tokens or refusal, become a failed Outcome instead of an exception that would stop the program.
  3. Outcome is a small dataclass that records whether the item succeeded, the text or error message, and the tokens used. That makes reporting straightforward.
  4. The list comprehension in main() processes every review with one shared client.
  5. The summary line counts successes with sum(outcome.ok ...), which works because True counts as 1 and False as 0, and totals the output tokens for cost tracking.

The "<sentiment> | <summary>" format makes the text easy to split later with text.split(" | ", 1). For guaranteed machine-readable output, use structured outputs (Lessons 101–110).


Common Mistakes

Passing Arguments Positionally

What happens: TypeError, because create() accepts only keyword arguments.

Fix: name every argument, or unpack a dictionary with **.

Passing None for Optional Parameters

What happens: type-checker warnings, or 400 errors when null is not accepted.

Fix: leave optional parameters out entirely when they have no value.

Sharing a Mutable Base Request

BASE = {"model": "claude-opus-5-5", "max_tokens": 1024, "messages": []}

def ask(question):
    BASE["messages"].append({"role": "user", "content": question})   # Modifies the shared list!
    return client.messages.create(**BASE)

What happens: every call adds to the same list, so later requests unexpectedly contain earlier questions, and may end up with invalid role order.

Fix: build a new dictionary and a new messages list for each request, as build_request() does.

Stopping Everything on the First Error

What happens: one failed item, such as a rate limit or an invalid input, aborts a long processing run.

Fix: put try/except inside the loop and record failures per item.

Ignoring the Response Status

What happens: truncated or refused responses are stored as valid summaries.

Fix: check stop_reason before accepting the text (Lesson 20).


Best Practices

  • Use keyword arguments, or build request dictionaries and unpack them with **.
  • Write request-builder functions that return a fresh dictionary for each request.
  • Add optional parameters only when needed.
  • Separate templates from data, and wrap data in tags.
  • Handle errors and stop reasons per item when creating many messages.
  • Track usage across a run for cost awareness.
  • Use async requests or batches for large volumes instead of long sequential loops.

Exercise

  1. Add a fourth review to REVIEWS that is written in another language, such as Tamil or Hindi. Does the summarizer still work, and in which language does it answer?
  2. Change build_request() to accept an optional max_tokens parameter, defaulting to 1024, and add it to the request.
  3. Modify the output so that each successful result is split into separate sentiment and summary values using text.split(" | ", 1). Handle the case where the separator is missing.
  4. Explain why BASE["messages"].append(...) in Common Mistakes causes later requests to contain earlier questions.
Show answers for steps 3 and 4

Step 3:

for outcome in outcomes:
    if not outcome.ok:
        print(f"[ERR] {outcome.review_id}: {outcome.text}")
        continue

    parts = outcome.text.split(" | ", 1)
    if len(parts) == 2:
        sentiment, summary = parts
        print(f"{outcome.review_id}: sentiment={sentiment.strip()} summary={summary.strip()}")
    else:
        print(f"{outcome.review_id}: unexpected format -> {outcome.text}")

split(" | ", 1) splits at most once, so a | inside the summary text does not break the result. The len(parts) == 2 check handles responses that do not follow the format.

Step 4: BASE["messages"] is a single list object shared by every call. append() modifies that list in place instead of creating a new one, so each call adds another user message to the same list, and every later request includes all earlier questions.

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 Create Messages and get answers drawn from it.

Signed-in readers only.