Extract Fields from Text

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

What Extraction Is

Extraction means pulling specific pieces of information out of text that already exists: the order number in an email, the date in an announcement, the total on an invoice. The text is the source, and the output must be grounded in it.

Extraction is the most common use of structured outputs in real applications, because businesses already have large amounts of unstructured text: emails, forms, reports, transcripts, and web pages. A schema turns that text into records a database or workflow can use.

"Hi, this is Ravi Kumar (order ORD-20391). Please send 2 Python Basics by 20 November 2026."
                                   │
                                   ▼  extraction schema
{"customer_name": "Ravi Kumar", "order_number": "ORD-20391",
 "items": [{"product": "Python Basics", "quantity": 2}],
 "requested_delivery_date": "2026-11-20"}

Lesson 106 focuses on extraction: getting facts from a source. Lesson 107 covers generation: creating new data that does not come from a source. The distinction matters, because the rules differ.


Extraction Rules

Good extraction follows four rules, and the schema and prompt should enforce them together:

RuleWhyHow
Use only what the source statesInvented values look correct and are dangerousSystem prompt: "Use only information the text states"
Use null for missing valuesGuessing fills gaps silentlyNullable fields, with a clear instruction
Normalize to a standard formDownstream code needs one formatDescribe the format: YYYY-MM-DD, whole rupees, exact order numbers
Ground the answer in evidenceMakes errors checkableAn evidence field with a quote copied from the source

Missing Values: Null, Not Guesses

Many extraction failures are not wrong formats, but plausible guesses. If an email says "next Tuesday", the extractor should not invent a calendar date. Make missing values explicit:

requested_delivery_date: str | None = Field(
    description="Requested date in YYYY-MM-DD format, or null if the text does not state a date"
)

The description explains both the format and the missing-value behavior. Claude sees the instruction, and the schema guarantees the field exists (Lesson 104).

Lists: Every Instance, or None

For repeated information, such as products in an order, use a list. Tell Claude to include every instance, and to return an empty list when there are none:

items: list[OrderItem] = Field(description="Every product the customer asks for; empty list if none")

Without the instruction, models sometimes summarize several products into one, or miss the last one.


Grounding with Evidence

A field such as customer_name can be wrong without any visible sign. An evidence field makes the extraction checkable: Claude copies a sentence from the source that supports the result, and your code confirms that the sentence really appears in the text:

evidence: str = Field(description="One sentence copied exactly from the email that shows the order request")

If the "evidence" is not in the source, the extraction is suspect. This check is cheap, objective, and catches many invented values. It does not prove every field correct, but it turns a hidden error into a visible one.

Note: the evidence check compares text after normalizing whitespace and case. It can reject a correct quote that Claude paraphrased, so use it as a signal for review or retry, not as the only truth.


Practical Example: Order Extraction from Emails

This example extracts order details from customer emails. It checks that dates are valid and quantities are positive, and verifies that the evidence quote appears in the email.

"""order_extraction.py: extract order details from customer emails."""

from datetime import date

from anthropic import Anthropic
from pydantic import BaseModel, Field, ValidationError

client = Anthropic()
MODEL = "claude-opus-5-5"

SYSTEM = (
    "You extract order details from customer emails. Use only information the email states. "
    "Never guess or infer a missing value: use null for any field the email does not provide. "
    "For the evidence field, copy one short sentence from the email exactly as written."
)


class OrderItem(BaseModel):
    product: str = Field(description="Product name, as written in the email")
    quantity: int | None = Field(description="Whole number of units; null if no quantity is stated")


class OrderEmail(BaseModel):
    customer_name: str | None = Field(description="Sender's full name, or null if not given")
    order_number: str | None = Field(description="Order number exactly as written, or null")
    items: list[OrderItem] = Field(description="Every product the customer asks for; empty list if none")
    requested_delivery_date: str | None = Field(description="Requested date in YYYY-MM-DD format, or null")
    evidence: str = Field(description="One sentence copied exactly from the email that shows the order request")


def normalize(text: str) -> str:
    return " ".join(text.split()).lower()


def check_order(order: OrderEmail, email_text: str) -> list[str]:
    problems = []
    if order.requested_delivery_date is not None:
        try:
            date.fromisoformat(order.requested_delivery_date)
        except ValueError:
            problems.append(f"delivery date {order.requested_delivery_date!r} is not YYYY-MM-DD")
    for item in order.items:
        if item.quantity is not None and item.quantity < 1:
            problems.append(f"quantity for {item.product!r} must be at least 1")
    if not order.evidence.strip():
        problems.append("evidence is empty")
    elif normalize(order.evidence) not in normalize(email_text):
        problems.append("evidence is not a sentence from the email")
    return problems


def extract_order(email_text: str) -> tuple[OrderEmail | None, list[str]]:
    try:
        response = client.messages.parse(
            model=MODEL,
            max_tokens=2048,
            system=SYSTEM,
            output_format=OrderEmail,
            messages=[{"role": "user", "content": f"<email>\n{email_text}\n</email>"}],
        )
    except ValidationError as error:
        return None, [f"response failed validation: {error.errors()[0]['msg']}"]
    if response.stop_reason != "end_turn":
        return None, [f"incomplete response (stop_reason={response.stop_reason})"]
    order = response.parsed_output
    return order, check_order(order, email_text)


EMAILS = [
    "Hi, this is Ravi Kumar (order ORD-20391). Please send 2 copies of Python Basics and "
    "1 SQL Workbook by Friday, 20 November 2026. Thanks!",
    "Can you send the Django starter kit when you get a chance? Thanks, Meena",
]

if __name__ == "__main__":
    for email_text in EMAILS:
        order, problems = extract_order(email_text)
        if order is None:
            print("Not extracted:", problems)
            continue
        print(f"customer: {order.customer_name}  order: {order.order_number}")
        for item in order.items:
            quantity = item.quantity if item.quantity is not None else "?"
            print(f"  {quantity} x {item.product}")
        print(f"  delivery: {order.requested_delivery_date}")
        if problems:
            print("  problems:", problems)

Expected output (exact wording varies):

customer: Ravi Kumar  order: ORD-20391
  2 x Python Basics
  1 x SQL Workbook
  delivery: 2026-11-20
customer: Meena  order: None
  ? x Django starter kit
  delivery: None

How It Works

  1. SYSTEM states the extraction rules: use only what the email says, use null for missing values, and copy the evidence exactly.
  2. OrderEmail combines the rules with the schema. Every field is nullable or a list, so missing information has a defined representation. The descriptions specify formats, such as YYYY-MM-DD, and the missing-value behavior.
  3. The email is wrapped in tags (Lesson 37), so its content is clearly data.
  4. check_order() performs three checks the schema cannot: that the delivery date is a real date, that quantities are positive, and that the evidence quote appears in the email.
  5. The second email has no order number, no quantity, and no date. Those fields are null, and the product is still extracted, with an unknown quantity shown as ? by the display code.
  6. Completeness is checked first: a truncated or refused response returns no order, rather than partial data.

Common Mistakes

Letting the Model Infer Missing Values

What happens: "next Tuesday" becomes a specific date that nobody provided.

Fix: instruct Claude to return null when the text does not state a value.

Asking for a Single Value When Several Exist

What happens: only one product from an order is extracted.

Fix: use a list, and say "every" in the description.

Vague Formats

What happens: dates arrive as 20/11/26, Nov 20, and 20th November in the same batch.

Fix: specify the exact format in the description, and validate it in code.

Trusting Extraction Without Evidence

What happens: invented values pass unnoticed.

Fix: require evidence, and check that it appears in the source.

Extracting from Text That Is Too Long for One Pass

What happens: values from the middle of a long document are missed.

Fix: split the document into sections, extract from each, and merge the results in code (Lesson 109).


Best Practices

  • Write the extraction rules in the system prompt: use only stated values, null when missing.
  • Describe formats and missing-value behavior in every field.
  • Use lists for repeated information, with "every" in the description.
  • Add an evidence field and verify it in code.
  • Validate formats and ranges that the schema cannot enforce.
  • Use Literal types for fixed categories.
  • Keep the source in tags, separate from instructions.

Exercise

  1. Add a delivery_address field (nullable) to OrderEmail, and test it with an email that includes an address.
  2. Write an email that mentions two different order numbers. What does the extraction return, and what check would you add?
  3. Change check_order() to reject an order with items but no customer name, and explain whether this should be a schema rule or a code rule.
  4. Explain why the evidence check uses normalize() on both strings.
Show answers for steps 2 and 4

Step 2: Claude may choose one order number, or may return the one that appears first. The schema cannot express "exactly one", so add a code check that counts order-number-like patterns in the text, for example with a regular expression such as ORD-\d+, and flags emails with more than one match for human review.

Step 4: whitespace and capitalization can differ between a quote copied by Claude and the original email, for example line breaks inside a sentence or a capital letter at the start. Normalizing both strings means the check compares the words, not the layout, so correct quotes are not rejected for trivial differences.

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

Signed-in readers only.