Extract Table Data

Su Suriya Ravichandran Updated 10 Nov 2026
7 min read ·Lesson 119 of 200

Tables Are Structured Data Hidden in Documents

Tables carry some of the most valuable information in business documents: invoices, price lists, financial statements, test results, and inventory counts. Their meaning comes from structure: which value belongs to which row, and under which column heading. Reading a table as plain text often loses that structure, and that is where extraction errors come from.

Claude can read tables in text, in PDFs, and in images. The extraction approach is the same in each case: describe the rows as a schema, ask for the values exactly as they appear, and check the results against the table's own arithmetic.


Describe Rows, Not Tables

A table is a list of rows, so the schema should describe one row, and the result should be a list of rows:

class InvoiceLine(BaseModel):
    product: str = Field(description="Product name, exactly as written in the table")
    quantity: int = Field(description="Quantity as a whole number")
    unit_price_inr: int = Field(description="Unit price in whole rupees")
    total_inr: int = Field(description="Line total in whole rupees, exactly as written in the table")


class InvoiceTable(BaseModel):
    lines: list[InvoiceLine] = Field(description="Every data row of the table, in order; no header or total rows")
    grand_total_inr: int | None = Field(description="The grand total from the table's total row, or null if none")

Two details matter:

  • Exclude header and total rows from lines. Totals look like products to a model that is not told otherwise, and would be double-counted in later checks.
  • Copy values exactly. Tell Claude not to correct arithmetic. If a source table contains an error, the extraction should preserve it, so that your check can detect it. A "helpful" correction would hide the error.

Checking Tables Against Themselves

Tables contain redundant information: a line total should equal quantity times unit price, and the lines should add up to the grand total. These relationships give you a free, objective check on the extraction, and on the source:

CheckMeaning
quantity × unit_price = total for each lineThe row is internally consistent
Sum of line totals = grand totalThe table adds up
Row count matches what the document saysNo rows were missed

If an extraction fails these checks, either the extraction is wrong, or the source table is inconsistent. Both are worth knowing about, and a person should review them.


Practical Example: Checking an Invoice Table

This example extracts an invoice table from text, then checks each row and the total. The sample table contains one deliberate arithmetic error, so the checks have something to find.

"""invoice_table.py: extract a table as rows, then check the arithmetic."""

from anthropic import Anthropic
from pydantic import BaseModel, Field

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

TABLE = """Invoice INV-2026-118, 9 November 2026

| Product          | Qty | Unit price (INR) | Line total (INR) |
|------------------|-----|------------------|------------------|
| Python Basics    | 2   | 499              | 1000             |
| SQL Workbook     | 1   | 899              | 899              |
| Django Starter   | 3   | 1200             | 3600             |

Grand total (INR): 5599
"""


class InvoiceLine(BaseModel):
    product: str = Field(description="Product name, exactly as written in the table")
    quantity: int = Field(description="Quantity as a whole number")
    unit_price_inr: int = Field(description="Unit price in whole rupees")
    total_inr: int = Field(description="Line total in whole rupees, exactly as written in the table")


class InvoiceTable(BaseModel):
    lines: list[InvoiceLine] = Field(description="Every data row of the table, in order; no header or total rows")
    grand_total_inr: int | None = Field(description="The grand total from the table's total line, or null")


def extract_table(text: str) -> InvoiceTable:
    response = client.messages.parse(
        model=MODEL,
        max_tokens=2048,
        system="Extract the table exactly as written. Copy every value as it appears, and do not "
               "correct any arithmetic, even if a value looks wrong.",
        output_format=InvoiceTable,
        messages=[{"role": "user", "content": f"<document>\n{text}\n</document>"}],
    )
    if response.stop_reason != "end_turn" or response.parsed_output is None:
        raise RuntimeError(f"no table extracted (stop_reason={response.stop_reason})")
    return response.parsed_output


def check_table(table: InvoiceTable) -> list[str]:
    problems = []
    for line in table.lines:
        expected = line.quantity * line.unit_price_inr
        if expected != line.total_inr:
            problems.append(f"{line.product}: {line.quantity} x {line.unit_price_inr} = {expected}, "
                            f"but the table says {line.total_inr}")
    if table.grand_total_inr is not None:
        computed = sum(line.total_inr for line in table.lines)
        if computed != table.grand_total_inr:
            problems.append(f"lines add up to {computed}, but the grand total says {table.grand_total_inr}")
    return problems


if __name__ == "__main__":
    table = extract_table(TABLE)
    for line in table.lines:
        print(f"{line.product:<16} {line.quantity:>3} x {line.unit_price_inr:>5} = {line.total_inr:>5}")
    print("grand total:", table.grand_total_inr)
    print("problems:", check_table(table) or "none")

Expected output (the extracted rows should match the table; the checks find two problems):

Python Basics      2 x   499 =  1000
SQL Workbook       1 x   899 =   899
Django Starter     3 x  1200 =  3600
grand total: 5599
problems: ['Python Basics: 2 x 499 = 998, but the table says 1000', 'lines add up to 5499, but the grand total says 5599']

How It Works

  1. InvoiceLine describes one row, and InvoiceTable holds the list of rows plus the grand total. The total is nullable, because not every table has one.
  2. The system prompt forbids correction. Claude is asked to copy values exactly, which is what makes the check meaningful. A table with an arithmetic error must come out with the error still in it.
  3. extract_table() raises on incomplete responses, so a truncated table is never mistaken for a complete one.
  4. check_table() verifies each line. For the first product, 2 × 499 = 998, but the table says 1000. The check reports the discrepancy with both numbers, so a person can compare them with the source.
  5. The total check adds the line totals as the table states them (1000 + 899 + 3600 = 5499) and compares the sum with the stated grand total of 5599. The two problems have different causes: one row is wrong, and the table's total does not match its own rows.
  6. The printed rows confirm the extraction before the checks run, which is useful during development.

Tables in PDFs and Images

The same approach applies to a table in a PDF or image (Lessons 111–114). Send the document as the content block, and keep the schema and checks unchanged. Scanned or photographed tables, however, are more error-prone than digital tables, because misread digits break the arithmetic checks. The checks are especially valuable there, because they expose misreadings that would otherwise go unnoticed. When a check fails, ask Claude to re-read the table, or send the table for human verification.


Common Mistakes

Including Header and Total Rows

What happens: totals are counted as products, and sums double.

Fix: state explicitly that only data rows belong in lines.

Asking Claude to Correct Arithmetic

What happens: source errors disappear, and the checks pass for the wrong reason.

Fix: instruct Claude to copy values exactly, and check the arithmetic in code.

Describing the Table as Text Instead of Rows

What happens: the output is a paragraph, not records that code can use.

Fix: use a row model and a list.

Losing Column Meaning in Merged Cells

What happens: values shift into the wrong columns, or repeat across merged rows.

Fix: describe how merged cells should be handled, and verify the checks.

Trusting Scanned Tables Without Checks

What happens: a misread digit goes into the database unnoticed.

Fix: use arithmetic checks, and route failures to review.


Best Practices

  • Model one row, and return a list.
  • Exclude headers and totals from the rows, and capture totals separately.
  • Copy values exactly, and never ask for corrections.
  • Verify arithmetic in code, since tables carry their own redundancy.
  • Report problems with both numbers, so reviewers can compare.
  • Route failed checks to review rather than storing them silently.

Exercise

  1. Change the table's Django Starter total from 3600 to 3500, and run the checks. Which problems appear, and what does each one tell you?
  2. Add a row_count_in_document field to InvoiceTable, and check that it equals len(lines).
  3. Write a check that every product name is unique, and explain whether it belongs in the schema or in code.
  4. Explain why the system prompt tells Claude not to correct arithmetic, even though correcting it would produce a cleaner result.
Show answers for steps 1 and 4

Step 1: two problems appear. First, the Django Starter row is internally inconsistent: 3 × 1200 = 3600, but the table now says 3500. Second, the line totals now add up to 1000 + 899 + 3500 = 5399, which does not match the grand total of 5599. The Python Basics row still shows its own problem, so the checks report three messages in total. Each message points to a different cause, and the fix belongs in the source document, not in the extraction.

Step 4: a corrected value would look like good data, and the arithmetic check would then pass. The error would still be in the source, but nothing would reveal it, and a person might trust a total that does not reflect the document. Preserving the value keeps the discrepancy visible, which is the purpose of the check. Correction is a decision for a human reviewer, not a silent step in the extraction.

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 Table Data and get answers drawn from it.

Signed-in readers only.