Process Text Files

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

Text Is Often the Simplest Document

Not every document needs the PDF path from Lesson 114. Many useful documents are already plain text: meeting notes, log files, exported chat transcripts, Markdown documentation, code, or CSV exports. Plain text is smaller than a PDF of the same content, cheaper to send, and easier to check, so when the content is text, send it as text.

A text document is a document block with a text source. The data is the text itself, not base64, and the media type is text/plain:

{
    "type": "document",
    "source": {
        "type": "text",
        "media_type": "text/plain",
        "data": "Meeting notes, 9 November...\n\nDecision: ship the beta on Friday.",
    },
    "title": "Meeting notes",
}

The title is optional, but it helps Claude refer to the document when it answers, and it helps you identify the source in logs.

This form is the same one used with citations in Lesson 41, where the document's text can be cited in the answer.


Reading Text Files Correctly

Text files are not always as simple as they look. Three problems come up in practice:

ProblemWhat happensFix
Unknown encodingAccented letters or rupee signs turn into garbageDecode as UTF-8, and handle a byte-order mark (BOM)
Binary contentA file that is not text, such as an image renamed to .txt, produces nonsenseReject files containing NUL bytes
Mixed line endingsWindows and Unix line endings appear togetherNormalize to \n
def read_text(data: bytes) -> str:
    if b"\x00" in data:
        raise ValueError("this file looks binary, not text")
    text = data.decode("utf-8-sig")       # utf-8-sig removes a byte-order mark if one exists
    return text.replace("\r\n", "\n").replace("\r", "\n")
  • utf-8-sig strips the byte-order mark that some Windows editors add at the start of a file. Without it, the first word can be preceded by an invisible character that confuses parsing.
  • A NUL byte almost never appears in real text, so its presence is a reliable sign of binary data.
  • UnicodeDecodeError is raised for files that are not valid UTF-8. Catch it and report the problem, rather than guessing an encoding, which can silently corrupt text.

Checking Whether the Text Fits

Long text uses many tokens. Count them before sending, using the same approach as Lesson 46, so you know whether a single request is enough:

def fits(client, model: str, text: str, budget: int = 150_000) -> bool:
    count = client.messages.count_tokens(
        model=model,
        messages=[{"role": "user", "content": [
            {"type": "document", "source": {"type": "text", "media_type": "text/plain", "data": text}}
        ]}],
    )
    return count.input_tokens <= budget

The budget is a local policy, chosen to keep requests predictable. Choose it for your application. Documents that do not fit should be split and processed in parts, which is the subject of Lesson 118.


Practical Example: Asking About a Text File

This example reads a text file, checks that it is text and that it fits within the budget, and asks a question about it. It uses a short sample built in memory, so it runs as written.

"""text_document.py: send a plain-text file as a document and ask about it."""

from anthropic import Anthropic

client = Anthropic()
MODEL = "claude-opus-5-5"
TOKEN_BUDGET = 150_000


def read_text(data: bytes) -> str:
    if b"\x00" in data:
        raise ValueError("this file looks binary, not text")
    text = data.decode("utf-8-sig")
    return text.replace("\r\n", "\n").replace("\r", "\n")


def ask_about_text(data: bytes, question: str, title: str) -> str:
    text = read_text(data)
    document = {
        "type": "document",
        "source": {"type": "text", "media_type": "text/plain", "data": text},
        "title": title,
    }
    count = client.messages.count_tokens(model=MODEL, messages=[{"role": "user", "content": [document]}])
    if count.input_tokens > TOKEN_BUDGET:
        raise ValueError(f"{title} is {count.input_tokens:,} tokens; the budget is {TOKEN_BUDGET:,}")

    message = client.messages.create(
        model=MODEL,
        max_tokens=1024,
        messages=[{"role": "user", "content": [document, {"type": "text", "text": question}]}],
    )
    return "".join(block.text for block in message.content if block.type == "text").strip()


if __name__ == "__main__":
    notes = (
        "\ufeffMeeting notes, 9 November 2026\r\n"
        "Attendees: Asha, Ravi, Meena\r\n\r\n"
        "Decision: ship the beta on Friday.\r\n"
        "Action: Ravi writes the release checklist by Wednesday.\r\n"
        "Open question: who handles support during launch week?\r\n"
    ).encode("utf-8")

    print(ask_about_text(notes, "List the action items and their owners.", "Meeting notes"))

Expected output (wording varies):

- Ravi: write the release checklist by Wednesday.

How It Works

  1. read_text() rejects binary data first, then decodes with utf-8-sig, which removes the byte-order mark added to the sample deliberately with \ufeff.
  2. Line endings are normalized so \r\n from Windows and \n from Unix become one form.
  3. The document block uses the text source, with text/plain as the media type and a title for reference.
  4. count_tokens() checks the size before the real request, and stops the work with a clear message if the document is too large.
  5. The document comes before the question, following the same ordering as images and PDFs.

Common Mistakes

Decoding with the Wrong Encoding

What happens: accented names, currency symbols, and non-English text appear as garbage, and the model answers from corrupted content.

Fix: decode explicitly as UTF-8, and report decoding errors.

Ignoring the Byte-Order Mark

What happens: the first line is preceded by an invisible character, which can break heading detection or exact-match checks.

Fix: use utf-8-sig.

Sending Binary Files as Text

What happens: the model receives unreadable data, and answers confidently about nothing.

Fix: reject files containing NUL bytes, and check the file type.

Sending Very Long Text Without Counting

What happens: the request fails, or costs far more than expected.

Fix: count tokens first, and split large documents (Lesson 118).

Losing the Document's Identity

What happens: answers mention "the document" without saying which one, and logs cannot tell documents apart.

Fix: set a title, and log the document's name or content hash.


Best Practices

  • Send text as text when the content is already text.
  • Decode explicitly, with utf-8-sig, and handle errors.
  • Normalize line endings before sending.
  • Reject binary data early.
  • Count tokens before sending long documents.
  • Give documents titles for clarity and traceability.

Exercise

  1. Save the sample as a file encoded in Latin-1 (for example with "Ravi, Café".encode("latin-1")). What happens when read_text() runs on it, and how should the function respond?
  2. Write a function that reads a Markdown file and removes its front matter (the block between --- lines at the top) before sending.
  3. Add a check that rejects text files with more than 50,000 lines, and explain why a line limit can be a useful complement to a token budget.
  4. Explain why read_text() checks for NUL bytes before decoding, rather than after.
Show answers for steps 1 and 4

Step 1: a Latin-1 file usually contains bytes that are not valid UTF-8, so data.decode("utf-8-sig") raises UnicodeDecodeError. The function should catch that exception and return a clear message, such as "this file is not UTF-8; please save it as UTF-8 and try again". Trying other encodings automatically can produce plausible but wrong text, which is worse than an honest error.

Step 4: a binary file, such as an image, may contain bytes that are invalid UTF-8, so decoding it first would raise a confusing UnicodeDecodeError. Checking for NUL bytes first gives the more useful message, that the file looks binary. Checks run from the most definitive test to the least, so each failure explains the real problem.

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 Process Text Files and get answers drawn from it.

Signed-in readers only.