Create Messages
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 asmax_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
)
| Option | Purpose |
|---|---|
timeout | Overrides the client's timeout for this request |
extra_headers | Adds HTTP headers to this request |
extra_query | Adds URL query parameters |
extra_body | Adds 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:
- Reuse one client for all requests (Lesson 10).
- Isolate errors per item, so one failure does not stop the whole run.
- 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
build_request()returns a fresh request dictionary for each review, combining the fixed system prompt and template with the variable review text.summarize()isolates everything that can go wrong for one review. API errors and unexpected stop reasons, such asmax_tokensorrefusal, become a failedOutcomeinstead of an exception that would stop the program.Outcomeis a small dataclass that records whether the item succeeded, the text or error message, and the tokens used. That makes reporting straightforward.- The list comprehension in
main()processes every review with one shared client. - The summary line counts successes with
sum(outcome.ok ...), which works becauseTruecounts as 1 andFalseas 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
- Add a fourth review to
REVIEWSthat is written in another language, such as Tamil or Hindi. Does the summarizer still work, and in which language does it answer? - Change
build_request()to accept an optionalmax_tokensparameter, defaulting to 1024, and add it to the request. - Modify the output so that each successful result is split into separate
sentimentandsummaryvalues usingtext.split(" | ", 1). Handle the case where the separator is missing. - Explain why
BASE["messages"].append(...)in Common Mistakes causes later requests to contain earlier questions.
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.