Send User Messages
What a User Message Is
A user message is a turn in the conversation that comes from the user side: the question, task, or data you want Claude to respond to. Every request contains at least one:
messages = [
{"role": "user", "content": "Explain what a Python virtual environment is."}
]
Each message is a dictionary with two keys:
| Key | Value | Meaning |
|---|---|---|
role | "user" | This turn comes from the user side of the conversation |
content | A string or a list of content blocks | What the user is saying or providing |
"User side" does not always mean a human typed it. Your application often writes user messages itself, for example by combining a fixed instruction with a document to summarize. The user role simply marks the input Claude should respond to. Your application's persistent rules belong in the system parameter (Lesson 17).
Rules for the messages List
The messages parameter is a list of turns in chronological order. A few structural rules apply.
1. The Roles
{"role": "user", "content": "..."} # Input to respond to
{"role": "assistant", "content": "..."} # Claude's earlier replies
These two roles make up the conversation itself. On current models, a third role, "system", can also appear inside messages to add an operator instruction partway through a conversation, subject to placement rules (Lessons 17 and 23). Any other value, such as "human", "bot", or "ai", is rejected.
2. Conversations Start with a User Message
The first turn is the user's input. Claude's replies follow it. Instructions that apply from the start go in the top-level system parameter, not in the first message.
3. The Conversation Must End with User Input
On current models (Claude 4.6 and later), the conversation must end with user input. That is normally a user message, or a mid-conversation system message placed directly after a user message. Ending with an assistant message, a technique called prefill that older models supported, returns a 400 invalid_request_error:
This model does not support assistant message prefill. The conversation must end with a user message.
Why it matters: older tutorials sometimes end the list with a partial assistant message to force the start of the reply, such as {"role": "assistant", "content": "{"} to force JSON. That no longer works on current models. Use instructions or structured outputs (Lessons 101–110) instead.
4. Turns Normally Alternate
A conversation usually alternates user, assistant, user, assistant, … If you send two consecutive messages with the same role, the API combines them into a single turn. It is clearer to build one message with several content blocks yourself.
messages = [
{"role": "user", "content": "My name is Priya."},
{"role": "assistant", "content": "Nice to meet you, Priya!"},
{"role": "user", "content": "What is my name?"}, # Last message: user
]
Multi-turn conversations are covered in Lessons 23–28. This lesson focuses on writing and building the user messages themselves.
The Two Forms of content
String Form
{"role": "user", "content": "Summarize the benefits of unit testing."}
This is the simplest form, for messages that are just text.
Block Form
{
"role": "user",
"content": [
{"type": "text", "text": "Summarize the benefits of unit testing."},
],
}
As Lesson 16 explained, the string is shorthand for a list containing one text block. Use the block form when a message needs:
- several parts, such as an instruction plus data,
- non-text content, such as images or documents (Lessons 111–120),
- per-block settings, such as prompt caching (Lessons 121–130), or
- tool results (Lesson 87).
{
"role": "user",
"content": [
{"type": "image", "source": {"type": "url", "url": "https://example.com/diagram.png"}},
{"type": "text", "text": "Explain what this architecture diagram shows."},
],
}
Writing Effective User Messages
The quality of Claude's answer depends heavily on the quality of the request. The Prompting section (Lessons 31–40) covers this in depth. Three principles matter from the start.
1. Be Specific About the Task
Vague: Tell me about lists.
Specific: Explain Python lists to a beginner: how to create one, add items, and loop
over it. Include one short code example for each.
The specific version states the audience, the scope, and the expected output, so Claude does not have to guess.
2. Separate Instructions from Data
When a message contains both an instruction and data, such as a document, an email, or code, mark clearly where the data begins and ends. XML-style tags work very well with Claude:
email = "Hi team, the deploy is moved to Friday. Please update your tickets. – Ravi"
content = f"""Extract the action items from the email below.
<email>
{email}
</email>
Return a numbered list."""
Why delimiters help:
- Claude knows exactly which text is the data and which is the instruction.
- If the data itself contains instruction-like sentences, such as "Ignore previous instructions", they are clearly marked as content to analyze, not commands. This reduces the risk of prompt injection (Lessons 40 and 175).
- Tag names such as
<email>and<document>let you refer to the data precisely: "in the email above…"
3. Put Long Data Before the Question
When sending a long document, place the document first and the question or instruction after it:
content = f"""<report>
{long_report_text}
</report>
Based on the report above, what were the three biggest risks identified?"""
Placing the question at the end of long inputs tends to produce better answers, and it reads naturally: first the material, then the question about it.
Building User Messages from Real Input
In applications, user messages are built from text typed by people, uploaded files, or database records. That input must be validated before it is sent.
MAX_CHARS = 20_000
def build_user_message(text: str) -> dict:
"""Validate raw user input and return a user message."""
cleaned = text.strip()
if not cleaned:
raise ValueError("Message cannot be empty.")
if len(cleaned) > MAX_CHARS:
raise ValueError(f"Message is too long ({len(cleaned)} characters; limit {MAX_CHARS}).")
return {"role": "user", "content": cleaned}
Why each check exists:
| Check | Reason |
|---|---|
.strip() | Removes accidental leading and trailing whitespace and newlines |
| Empty check | The API rejects empty text content with a 400 error. Catching it first gives the user a clear message without a wasted request. |
| Length limit | Prevents accidental or abusive huge inputs that increase cost and latency. The right limit depends on your application. |
A character limit is a simple first protection. For precise control, count tokens before sending (Lesson 46).
Practical Example: Ask Questions About a Text File
This tool reads a text file, wraps it in tags, and asks Claude a question about it. It combines validation, the instruction/data separation, and the document-first layout.
"""ask_file.py: ask Claude a question about the contents of a text file.
Usage:
python ask_file.py notes.txt "What decisions were made?"
"""
import sys
from pathlib import Path
import anthropic
from anthropic import Anthropic
from anthropic.types import Message
MAX_CHARS = 200_000
SYSTEM_PROMPT = (
"You answer questions about documents. Base answers only on the document provided. "
"If the document does not contain the answer, say so clearly."
)
def get_text(message: Message) -> str:
return "".join(block.text for block in message.content if block.type == "text")
def build_question_message(filename: str, document: str, question: str) -> dict:
document = document.strip()
question = question.strip()
if not document:
raise ValueError("The file is empty.")
if not question:
raise ValueError("The question is empty.")
if len(document) > MAX_CHARS:
raise ValueError(f"The file is too large ({len(document):,} characters).")
content = f"""<document name="{filename}">
{document}
</document>
Answer this question about the document above:
<question>{question}</question>"""
return {"role": "user", "content": content}
def main() -> int:
if len(sys.argv) != 3:
print('Usage: python ask_file.py <file> "<question>"', file=sys.stderr)
return 1
path, question = Path(sys.argv[1]), sys.argv[2]
try:
document = path.read_text(encoding="utf-8")
user_message = build_question_message(path.name, document, question)
except (OSError, UnicodeDecodeError, ValueError) as error:
print(f"Error: {error}", file=sys.stderr)
return 1
client = Anthropic()
try:
reply = client.messages.create(
model="claude-opus-5-5",
max_tokens=4096,
system=SYSTEM_PROMPT,
messages=[user_message],
)
except anthropic.APIError as error:
print(f"API error: {error}", file=sys.stderr)
return 1
print(get_text(reply).strip())
return 0
if __name__ == "__main__":
sys.exit(main())
Create a sample file meeting.txt:
Sprint review – 3 Nov
- Payment page redesign approved; Anita leads it.
- Mobile release delayed one week due to a login bug.
- Decision: move weekly sync from Monday to Tuesday.
Run:
python ask_file.py meeting.txt "What decisions were made, and who owns them?"
Example output:
Two decisions were made:
1. The payment page redesign was approved, and Anita leads it.
2. The weekly sync moves from Monday to Tuesday (no owner is named).
The mobile release was also delayed by one week because of a login bug.
How It Works
- Reading the file uses
encoding="utf-8". Errors such as a missing file (OSError) or a non-text file (UnicodeDecodeError) produce a clear message instead of a traceback. build_question_message()validates both inputs, then builds the content with the document first, wrapped in<document name="...">tags, and the question last, in<question>tags. Thenameattribute tells Claude which file it is reading.- The system prompt tells Claude to rely only on the document and to say when the answer is not there. This reduces the chance of answers based on assumptions rather than the file.
messages=[user_message]sends a single user turn. This is a one-shot question, not a conversation.anthropic.APIErrorcatches every API-related failure in one place, which is enough for a small tool.
Notice the answer: it reports that no owner is named for one decision instead of inventing one. That is the effect of the grounding instruction in the system prompt.
Common Mistakes
Ending the List with an Assistant Message
What happens: 400 invalid_request_error: the model does not support assistant message prefill.
Fix: make sure the conversation ends with user input. To control output format, use instructions or structured outputs instead of prefill.
Sending Empty Content
What happens: an empty string or whitespace-only input causes a 400 error.
Fix: validate and strip input before sending.
Wrong Role Names or Shapes
{"role": "human", "content": "Hi"} # Invalid role
{"role": "user", "text": "Hi"} # Wrong key: must be "content"
messages={"role": "user", "content": "Hi"} # Must be a list
Fix: messages=[{"role": "user", "content": "Hi"}].
Mixing Instructions and Data Without Delimiters
What happens: Claude may confuse the data with your instructions, especially if the data contains imperative sentences. That is a prompt-injection risk.
Fix: wrap data in clearly named tags and keep your instructions outside them.
Putting Persistent Rules in Every User Message
What happens: repetition, higher token usage, and weaker priority than system instructions.
Fix: move rules that always apply into the system parameter.
Sending Unnecessary Personal or Sensitive Data
What happens: private data is transmitted and processed without need.
Fix: send only the data the task requires. Remove or mask personal identifiers that the task does not need.
Best Practices
- Validate input: strip it, reject empty messages, and limit size.
- Be specific about the task, the audience, and the expected output.
- Separate data from instructions with XML-style tags.
- Place long documents first and the question last.
- End every request with user input.
- Use the block form for images, documents, multi-part content, and caching.
- Keep rules in the system prompt and the specific request in the user message.
- Send only the data you need.
Exercise
- Rewrite this vague request into a specific one: "Help me with my code." Include a code snippet in
<code>tags. - Send a request whose
messageslist ends with an assistant message. Record the error, then fix the request. - Extend
build_user_message()to reject input that consists only of punctuation or symbols, such as"???". - Modify
ask_file.pyto accept two files and compare them, wrapping each one in its own<document>tag.
Step 2: the API returns 400 invalid_request_error, saying the model does not support assistant message prefill and that the conversation must end with a user message. Fix it by removing the trailing assistant message, or by adding a user message after it.
Step 3:
if not any(character.isalnum() for character in cleaned):
raise ValueError("Message must contain letters or numbers.")
str.isalnum() is true for letters and digits in any script, so non-English input is still accepted.
Step 4 (core part):
content = f"""<document name="{first.name}">
{first_text}
</document>
<document name="{second.name}">
{second_text}
</document>
Compare the two documents above. List what they agree on and where they differ."""
Both documents come first, each in its own named tag, followed by the instruction. Claude can then refer to each document by name in its answer.