Structured Data Extraction with Claude

Build a Python CLI that extracts validated JSON from messy text using Claude.

Introduction

30 Second Summary

Every day, valuable information hides inside messy text. Contact details buried in emails, totals scattered across invoices, opinions mixed into product reviews. Manually copying these into spreadsheets or databases is slow and error-prone.

In this project, you will build a Python command-line tool that uses Claude's API to automatically extract structured data from unstructured text. You will define schemas, send text to the API, and get back clean, validated JSON every time.

What You'll Build

A Python CLI data extraction pipeline that takes raw text (emails, invoices, reviews) and uses Claude's structured outputs feature to return validated JSON matching schemas you define with Pydantic models.

By the end of this project, you'll have:

  • An extraction engine that calls Claude's API with structured output constraints to guarantee valid JSON.
  • Pydantic data models defining extraction schemas for contacts, invoices, and product reviews.
  • A CLI interface that accepts messy text input and outputs clean, schema-validated JSON.
  • Secret Mission: add batch processing that reads multiple text files from a folder and outputs a consolidated CSV report.

Are there any prerequisites?

This project assumes intermediate Python knowledge. You should be comfortable with functions, classes, pip, and virtual environments.

Set Up Your Project

Before you can extract structured data from text, you need a Python project with the right tools installed and a connection to Claude's API.

In this step, you'll create your project environment, install the Anthropic Python SDK, and verify that your API key works with a test call.

In this step, get ready to:
  • Create your project directory and install dependencies.
  • Set up your Anthropic API account and generate an API key.
  • Verify your API connection with a test call to Claude.
Create your project directory and install dependencies

Every Python project starts with an isolated environment. A virtual environment keeps your project's packages separate from your system Python, preventing version conflicts.

  • Open VS Code by pressing Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type VS Code and press Enter to open it.
  • Open the integrated terminal by pressing Ctrl+` (the backtick key, above Tab).
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir extraction-pipeline
cd extraction-pipeline
  • Confirm the folder exists by running:
ls

You should see the extraction-pipeline folder listed in the output from the previous ls on Desktop. Now you are inside it.

  • Create a virtual environment by running:
python3 -m venv .venv

What is a virtual environment?

A virtual environment is an isolated Python installation inside your project folder. Packages you install here stay separate from your system Python, so different projects can use different library versions without conflicting.

  • Activate the virtual environment:

macOS

source .venv/bin/activate

Windows

.venv\Scripts\activate

You should see (.venv) appear at the beginning of your terminal prompt. This confirms the virtual environment is active.

Don't see (.venv) in your prompt?

On Windows, you may need to allow script execution first. Run Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned in PowerShell, then try the activate command again.

On macOS, make sure you typed source before the path. Without it, the command will fail silently.

Help me activate my virtual environment

Now you'll create the file that lists your project's dependencies. This project uses two packages: the anthropic SDK for calling Claude's API, and Pydantic for defining data schemas.

  • In VS Code, create a new file by pressing Cmd+N (macOS) or Ctrl+N (Windows).
  • Save the file immediately with Cmd+S (macOS) or Ctrl+S (Windows) and name it requirements.txt inside your extraction-pipeline folder.
  • Paste the following into requirements.txt:
anthropic==0.112.0
pydantic==2.11.4

What does this file do?

A requirements.txt file pins exact package versions so anyone can reproduce your environment. The anthropic package is the official Python SDK for Claude's API. pydantic provides the data validation models that Claude's structured outputs feature uses.

  • Save requirements.txt.
  • Install the dependencies by running this command in your terminal:
pip install -r requirements.txt

You should see output showing both anthropic and pydantic being downloaded and installed, ending with a success message.

Seeing a permission error or install failure?

Make sure (.venv) appears in your terminal prompt. If it does not, your virtual environment is not active and pip is trying to install to your system Python.

Help me fix my pip install error

✔️ Awesome, I've got everything!

Great. Double check you have saved requirements.txt and both packages installed without errors.

ⓧ I'd like to double check the full code

Here is the complete requirements.txt file:

anthropic==0.112.0
pydantic==2.11.4
Create your Anthropic account and API key

To call Claude's API, you need an API key from Anthropic. New accounts receive free trial credits, so this project costs very little to run.

  • Open your browser and navigate to console.anthropic.com.
  • Click Sign Up to create a new account (or Sign In if you already have one).
  • Once signed in, navigate to the API Keys section in the console.
  • Click Create Key to generate a new API key.
  • Give it a name like extraction-pipeline so you can identify it later.
  • Copy the key immediately and store it somewhere safe (a password manager or a temporary note). You will not be able to see the full key again after closing this dialog.

Why can I only see my key once?

Anthropic only displays the full API key at the moment of creation for security. After you close the dialog, the console shows only a partial hint. If you lose the key, you can always create a new one and revoke the old one.

Set your API key and verify the connection

The Anthropic SDK automatically reads the ANTHROPIC_API_KEY environment variable. You need to set this in your terminal so the SDK can authenticate your requests.

  • Set the environment variable in your terminal:

macOS

export ANTHROPIC_API_KEY=your-api-key-here

Windows

$env:ANTHROPIC_API_KEY = "your-api-key-here"
  • Replace your-api-key-here with the actual API key you copied from the console.

How long does this variable last?

This environment variable only exists in your current terminal session. If you close and reopen the terminal, you will need to set it again. For this project, that is fine.

Now test that your key works by making a simple API call to Claude.

  • Verify the connection by running this command:
python -c "import anthropic; client = anthropic.Anthropic(); msg = client.messages.create(model='claude-haiku-4-5', max_tokens=100, messages=[{'role': 'user', 'content': 'Say hello in one sentence.'}]); print(msg.content[0].text)"

You should see a short greeting from Claude printed in your terminal. This confirms your API key, SDK installation, and network connection are all working.

Seeing an authentication error?

Double check that you replaced your-api-key-here with your actual key (no extra spaces or quotes around it on macOS).

If you see a message about invalid API key, try generating a new key in the console and setting the variable again.

Help me fix my Anthropic API authentication error

Your environment is ready and Claude is responding. Next up, you'll define the data schemas that tell Claude exactly what structure to extract from messy text.

Define Your Data Schemas

Your extraction pipeline needs to know exactly what shape of data to look for in unstructured text. Without a defined structure, an LLM might return different fields every time, making the output unpredictable.

Pydantic models solve this by defining exact schemas that Claude's structured outputs feature uses to guarantee valid output. In this step, you'll create three models that each represent a different type of data your pipeline can extract.

In this step, get ready to:
  • Create Pydantic models for contacts, invoices, and product reviews.
  • Add field descriptions and default values to guide Claude's extraction.
  • Register your schemas in a lookup dictionary for the CLI.
Create the ContactInfo model

Each schema is a Python class that inherits from Pydantic's BaseModel. The Field function lets you attach metadata like descriptions and default values to each field. Claude reads these descriptions to understand what data to extract from the text.

  • Create a new file called schemas.py in your extraction-pipeline folder and add the following code:
from pydantic import BaseModel, Field


class ContactInfo(BaseModel):
    name: str = Field(description="Full name of the person")
    email: str = Field(description="Email address")
    phone: str = Field(default="", description="Phone number if available")
    company: str = Field(default="", description="Company or organization name")
    role: str = Field(default="", description="Job title or role")

What does this code do?

The ContactInfo class defines five fields that Claude will try to extract from any contact-related text.

  • Fields like name and email are required (no default). Claude must always extract these.
  • Fields like phone, company, and role have default="". If the text doesn't mention them, Claude returns an empty string instead of failing.
  • The description strings tell Claude exactly what each field represents, improving extraction accuracy.
  • Save schemas.py.
  • Confirm the import works by running this in your terminal:
python -c "from schemas import ContactInfo; print(ContactInfo.model_fields.keys())"

You should see dict_keys(['name', 'email', 'phone', 'company', 'role']) printed in your terminal.

Seeing a ModuleNotFoundError?

  • Make sure your terminal is in the extraction-pipeline directory where schemas.py lives.
  • Check that your virtual environment is still activated. You should see (.venv) at the start of your terminal prompt.

Still stuck? Help me debug why Python can't find my schemas module.

Add the InvoiceData and ProductReview models

Your pipeline needs to handle more than just contacts. Next, you'll add two more models that cover financial documents and customer feedback.

  • In schemas.py, add the InvoiceData class below your ContactInfo class:
class InvoiceData(BaseModel):
    invoice_number: str = Field(description="Invoice or reference number")
    date: str = Field(description="Invoice date in YYYY-MM-DD format")
    total_amount: float = Field(description="Total amount in dollars")
    vendor_name: str = Field(description="Name of the vendor or seller")
    line_items: list[str] = Field(default_factory=list, description="List of items or services")

What's different about InvoiceData?

This model introduces two new patterns compared to ContactInfo.

  • total_amount uses float instead of str. Claude will extract the dollar amount as a number, not a string.
  • line_items uses list[str] with default_factory=list. This tells Claude to return a list of strings, defaulting to an empty list if no items are found.
  • Now add the ProductReview class below InvoiceData:
class ProductReview(BaseModel):
    product_name: str = Field(description="Name of the product being reviewed")
    rating: float = Field(description="Rating out of 5")
    sentiment: str = Field(description="Overall sentiment: positive, negative, or mixed")
    pros: list[str] = Field(default_factory=list, description="Positive aspects mentioned")
    cons: list[str] = Field(default_factory=list, description="Negative aspects mentioned")
    summary: str = Field(description="One-sentence summary of the review")

Why constrain sentiment to specific values?

The sentiment field's description says "positive, negative, or mixed". Claude reads this and constrains its output to one of those three options. This makes downstream processing predictable because you can filter or group reviews by sentiment without worrying about inconsistent labels.

  • Save schemas.py and verify all three models import correctly:
python -c "from schemas import ContactInfo, InvoiceData, ProductReview; print('All 3 models loaded')"

You should see All 3 models loaded printed in your terminal.

Seeing a SyntaxError?

  • Check that each class definition starts with class ClassName(BaseModel): and all field lines are indented by four spaces.
  • Make sure every Field(...) call has matching opening and closing parentheses.
  • Verify there's a blank line between each class definition.

Need more help? Help me fix a SyntaxError in my Pydantic models.

Create the schema registry

Your CLI needs a way to look up which model to use based on a simple keyword. A dictionary that maps friendly names to model classes makes this easy.

  • At the bottom of schemas.py, add the SCHEMAS dictionary:
SCHEMAS = {
    "contact": ContactInfo,
    "invoice": InvoiceData,
    "review": ProductReview,
}

What does the SCHEMAS dictionary do?

This dictionary maps short string keys to their corresponding Pydantic model classes. When a user types python main.py contact, the CLI will look up SCHEMAS["contact"] to get the ContactInfo class and pass it to Claude.

What kind of text will each schema extract from?

Each model targets a different type of messy, real-world text:

  • ContactInfo: email signatures, LinkedIn messages, business card text.
  • InvoiceData: scanned invoices, payment confirmations, billing emails.
  • ProductReview: customer reviews, forum posts, feedback surveys.

You'll add sample texts for each of these in a later step when you build the CLI.

  • Save schemas.py and confirm the full module works:
python -c "from schemas import SCHEMAS; print(list(SCHEMAS.keys()))"

You should see ['contact', 'invoice', 'review'] printed in your terminal.

Seeing an ImportError for SCHEMAS?

  • Make sure the SCHEMAS dictionary is at the module level (no indentation), not inside a class or function.
  • Check that the dictionary references the class names exactly: ContactInfo, InvoiceData, ProductReview.

Still stuck? Help me debug why SCHEMAS won't import from my schemas module.

✔️ Awesome, I've got everything!

Great. Double check you've saved schemas.py and move on to the next step.

ⓧ I'd like to double check the full code

Here's the complete schemas.py file for comparison:

from pydantic import BaseModel, Field


class ContactInfo(BaseModel):
    name: str = Field(description="Full name of the person")
    email: str = Field(description="Email address")
    phone: str = Field(default="", description="Phone number if available")
    company: str = Field(default="", description="Company or organization name")
    role: str = Field(default="", description="Job title or role")


class InvoiceData(BaseModel):
    invoice_number: str = Field(description="Invoice or reference number")
    date: str = Field(description="Invoice date in YYYY-MM-DD format")
    total_amount: float = Field(description="Total amount in dollars")
    vendor_name: str = Field(description="Name of the vendor or seller")
    line_items: list[str] = Field(default_factory=list, description="List of items or services")


class ProductReview(BaseModel):
    product_name: str = Field(description="Name of the product being reviewed")
    rating: float = Field(description="Rating out of 5")
    sentiment: str = Field(description="Overall sentiment: positive, negative, or mixed")
    pros: list[str] = Field(default_factory=list, description="Positive aspects mentioned")
    cons: list[str] = Field(default_factory=list, description="Negative aspects mentioned")
    summary: str = Field(description="One-sentence summary of the review")


SCHEMAS = {
    "contact": ContactInfo,
    "invoice": InvoiceData,
    "review": ProductReview,
}

Your schemas are ready to go. Next up, you'll build the extraction engine that sends unstructured text to Claude and gets back clean, validated data matching these models.

Build the Extraction Engine

You have three Pydantic models ready to describe the shape of your extracted data. Now you need the engine that actually calls Claude and returns validated results.

In this step, you will write the core function that sends unstructured text to Claude and gets back a typed Pydantic object. The key is Anthropic's structured outputs feature, which uses constrained decoding to guarantee the response matches your schema exactly.

In this step, get ready to:
  • Write a system prompt optimized for data extraction.
  • Build the extraction function using client.messages.parse() with the output_format parameter.
  • Understand how API errors propagate for downstream handling.
Create the extraction module with a system prompt

The system prompt tells Claude exactly how to behave as a data extractor. It sets boundaries so Claude only extracts information explicitly present in the text, rather than hallucinating values.

  • Create a new file called extractor.py in your extraction-pipeline folder.
  • Add the imports and system prompt constant by pasting this code:
import anthropic
from pydantic import BaseModel

# System prompt that constrains Claude to extract only what's in the text
SYSTEM_PROMPT = """You are a precise data extraction assistant. 
Extract structured information from the provided text. 
Only include information explicitly stated or strongly implied in the text.
If a field cannot be determined from the text, use an empty string for text fields, 
0 for numeric fields, or an empty list for list fields."""

What does this code do?

The SYSTEM_PROMPT constant defines Claude's role as a data extractor. It includes explicit fallback rules (empty strings, zeros, empty lists) so Claude never invents data that is not in the source text.

The BaseModel import from Pydantic is used for type annotations in the function you will write next.

  • Save extractor.py.
  • You won't see output yet. The next chunk adds the function that uses this prompt.
Write the extraction function

Anthropic's Python SDK provides a client.messages.parse() method that accepts a Pydantic model as the output_format parameter. This guarantees the response is valid JSON matching your schema.

  • In extractor.py, add the following function below the SYSTEM_PROMPT constant:
def extract_data(text: str, schema: type[BaseModel]) -> BaseModel:
    # Create a client (reads ANTHROPIC_API_KEY from environment automatically)
    client = anthropic.Anthropic()

    # Call Claude with structured output constraints
    response = client.messages.parse(
        model="claude-haiku-4-5",
        max_tokens=1024,
        system=SYSTEM_PROMPT,
        messages=[
            {
                "role": "user",
                "content": f"Extract the structured data from the following text:\n\n{text}",
            }
        ],
        output_format=schema,
    )

    # Return the parsed Pydantic model instance
    return response.parsed_output

What does this code do?

The extract_data function takes raw text and a Pydantic schema class, then returns a validated instance of that schema.

  • anthropic.Anthropic() creates a client that automatically reads your ANTHROPIC_API_KEY environment variable.
  • output_format=schema tells Claude to constrain its response to match the Pydantic model's JSON schema exactly.
  • response.parsed_output is the SDK's convenience accessor that returns the already-validated Pydantic object.

Why structured outputs instead of prompting for JSON?

When you ask an LLM to return JSON via prompting alone, it can produce malformed output, miss fields, or add extra keys. You would need to write your own parsing and validation logic to catch these failures.

Anthropic's structured outputs feature uses constrained decoding at the model level. This guarantees the response is always valid JSON that matches your schema. No post-processing or retry logic needed.

  • Save extractor.py.
  • Test the extraction engine by running this command in your terminal:
python -c "from extractor import extract_data; from schemas import ContactInfo; result = extract_data('Hi, I am Sarah Chen from TechFlow. Reach me at sarah@techflow.io', ContactInfo); print(result)"

You should see a ContactInfo object printed with the name, email, and company fields populated from the text.

Seeing an API error?

  • Check that your ANTHROPIC_API_KEY environment variable is still set in your current terminal session. If you opened a new terminal, you need to set it again.
  • Make sure your virtual environment is active. You should see (.venv) in your terminal prompt.
  • Verify you are inside the extraction-pipeline directory where extractor.py and schemas.py both exist.

Help me debug my extraction engine API call.

✔️ Awesome, I've got everything!

Great. Double check you have saved extractor.py and the test returned a populated ContactInfo object.

ⓧ I'd like to double check the full code

import anthropic
from pydantic import BaseModel

SYSTEM_PROMPT = """You are a precise data extraction assistant. 
Extract structured information from the provided text. 
Only include information explicitly stated or strongly implied in the text.
If a field cannot be determined from the text, use an empty string for text fields, 
0 for numeric fields, or an empty list for list fields."""


def extract_data(text: str, schema: type[BaseModel]) -> BaseModel:
    client = anthropic.Anthropic()

    response = client.messages.parse(
        model="claude-haiku-4-5",
        max_tokens=1024,
        system=SYSTEM_PROMPT,
        messages=[
            {
                "role": "user",
                "content": f"Extract the structured data from the following text:\n\n{text}",
            }
        ],
        output_format=schema,
    )

    return response.parsed_output
Understand how errors propagate

The extract_data function intentionally does not wrap the API call in a try/except block. This is a design choice that gives callers full control over error handling.

What errors can occur?

  • anthropic.APIError covers network issues, invalid API keys, and rate limit errors. The caller (your CLI in the next step) wraps the call in try/except to handle these gracefully.
  • Schema validation errors are eliminated by the structured outputs feature. Because Claude's response is constrained at the decoding level, you never get malformed JSON or missing required fields.
  • If Claude cannot fill a field from the text, it returns the default value you defined in your Pydantic model (empty string, zero, or empty list) rather than raising an error.

Your extraction engine is built and tested. Next up, you will wrap it in a command-line interface so you can extract data from any text with a single terminal command.

Create the CLI Interface

You have an extraction engine that talks to Claude and returns structured data. Now you need a way to actually use it.

In this step, you will build a command-line interface that ties everything together. The CLI accepts a schema type, feeds text to the extraction engine, and prints clean JSON output with a summary of how many fields were populated.

In this step, get ready to:
  • Build a CLI that accepts a schema choice and optional text input.
  • Format and display extracted JSON with pretty-printing.
  • Add an extraction summary showing which fields were populated.
Create the CLI file with sample data

The CLI needs realistic test data built in so you can verify the pipeline works without typing messy text every time you run it.

  • Create a new file called main.py in your extraction-pipeline folder.
  • Add the imports and sample text dictionary by pasting the following code:
import json
import sys

from extractor import extract_data
from schemas import SCHEMAS

SAMPLE_TEXTS = {
    "contact": """Hi, I'm Sarah Chen from TechFlow Inc. I'm the Senior Product Manager 
there. You can reach me at sarah.chen@techflow.io or call me at (415) 555-0192. 
Looking forward to discussing the partnership!""",
    "invoice": """INVOICE #INV-2024-0892
Date: 2024-11-15
From: CloudStack Solutions

Items:
- Annual software license: $2,400.00
- Premium support package: $600.00  
- Data migration service: $350.00

Total Due: $3,350.00""",
    "review": """I bought the NovaSound Pro headphones last month and I'm mostly happy. 
The sound quality is incredible - crystal clear highs and deep bass. Battery life 
is amazing too, lasting about 40 hours. However, they're a bit heavy for long 
sessions and the ear cups get warm after an hour. The price tag of $299 feels 
justified though. I'd give them a 4 out of 5.""",
}

What does this code do?

The imports bring in json for pretty-printing output, sys for reading command-line arguments, and the extract_data function and SCHEMAS dictionary you built in previous steps.

The SAMPLE_TEXTS dictionary holds one realistic unstructured text sample for each schema type. These serve as built-in test cases so you can run the tool immediately without providing your own text.

  • Save main.py.
Add the main function with argument parsing and output formatting

The main function is the entry point for your CLI. It parses command-line arguments, runs the extraction, and formats the output with a confidence summary.

  • In main.py, add the following code below the SAMPLE_TEXTS dictionary:
def main():
    if len(sys.argv) < 2:
        print("Usage: python main.py <schema_type> [text]")
        print(f"Available schemas: {', '.join(SCHEMAS.keys())}")
        print("\nIf no text is provided, a sample will be used.")
        sys.exit(1)

    schema_type = sys.argv[1].lower()

    if schema_type not in SCHEMAS:
        print(f"Error: Unknown schema '{schema_type}'")
        print(f"Available schemas: {', '.join(SCHEMAS.keys())}")
        sys.exit(1)

    if len(sys.argv) > 2:
        text = " ".join(sys.argv[2:])
    else:
        text = SAMPLE_TEXTS.get(schema_type, "")
        print(f"Using sample text for '{schema_type}'...\n")

    schema = SCHEMAS[schema_type]

    print(f"Extracting {schema_type} data...\n")

    try:
        result = extract_data(text, schema)
        output = result.model_dump()
        print("Extracted Data:")
        print(json.dumps(output, indent=2))

        filled_fields = sum(
            1 for v in output.values() if v and v != 0 and v != []
        )
        total_fields = len(output)
        print(f"\nExtraction summary: {filled_fields}/{total_fields} fields populated")

    except anthropic.APIError as e:
        print(f"API Error: {e.message}")
        sys.exit(1)


if __name__ == "__main__":
    main()

What does this code do?

  • The argument check ensures the user passes a schema type (contact, invoice, or review). If missing, it prints usage instructions and exits.
  • If extra arguments appear after the schema type, they are joined into a single text string. Otherwise, the matching sample text is used automatically.
  • The result.model_dump() call converts the Pydantic model into a plain dictionary. Then json.dumps(output, indent=2) pretty-prints it with 2-space indentation.
  • The extraction summary counts how many fields have meaningful values (not empty strings, not 0, not empty lists) so you can see at a glance how much data Claude extracted.
  • The except anthropic.APIError block catches any API failures (authentication errors, rate limits, server errors) and prints a readable message instead of a traceback.
  • Save main.py.

Why catch anthropic.APIError specifically?

All errors from the Anthropic SDK inherit from anthropic.APIError. This includes authentication failures, rate limit errors, and server errors. Catching the base class means your tool handles every API failure gracefully with a single handler.

The .message attribute gives you a human-readable error description without needing to parse the HTTP response yourself.

Seeing a NameError for anthropic?

The main() function references anthropic.APIError but the anthropic module is not explicitly imported in main.py. If you see a NameError, add import anthropic at the top of main.py.

Help me fix this NameError.

✔️ Awesome, I've got everything!

Great. Double check you have saved main.py with both the SAMPLE_TEXTS dictionary and the main() function.

ⓧ I'd like to double check the full code

import json
import sys

from extractor import extract_data
from schemas import SCHEMAS

SAMPLE_TEXTS = {
    "contact": """Hi, I'm Sarah Chen from TechFlow Inc. I'm the Senior Product Manager 
there. You can reach me at sarah.chen@techflow.io or call me at (415) 555-0192. 
Looking forward to discussing the partnership!""",
    "invoice": """INVOICE #INV-2024-0892
Date: 2024-11-15
From: CloudStack Solutions

Items:
- Annual software license: $2,400.00
- Premium support package: $600.00  
- Data migration service: $350.00

Total Due: $3,350.00""",
    "review": """I bought the NovaSound Pro headphones last month and I'm mostly happy. 
The sound quality is incredible - crystal clear highs and deep bass. Battery life 
is amazing too, lasting about 40 hours. However, they're a bit heavy for long 
sessions and the ear cups get warm after an hour. The price tag of $299 feels 
justified though. I'd give them a 4 out of 5.""",
}


def main():
    if len(sys.argv) < 2:
        print("Usage: python main.py <schema_type> [text]")
        print(f"Available schemas: {', '.join(SCHEMAS.keys())}")
        print("\nIf no text is provided, a sample will be used.")
        sys.exit(1)

    schema_type = sys.argv[1].lower()

    if schema_type not in SCHEMAS:
        print(f"Error: Unknown schema '{schema_type}'")
        print(f"Available schemas: {', '.join(SCHEMAS.keys())}")
        sys.exit(1)

    if len(sys.argv) > 2:
        text = " ".join(sys.argv[2:])
    else:
        text = SAMPLE_TEXTS.get(schema_type, "")
        print(f"Using sample text for '{schema_type}'...\n")

    schema = SCHEMAS[schema_type]

    print(f"Extracting {schema_type} data...\n")

    try:
        result = extract_data(text, schema)
        output = result.model_dump()
        print("Extracted Data:")
        print(json.dumps(output, indent=2))

        filled_fields = sum(
            1 for v in output.values() if v and v != 0 and v != []
        )
        total_fields = len(output)
        print(f"\nExtraction summary: {filled_fields}/{total_fields} fields populated")

    except anthropic.APIError as e:
        print(f"API Error: {e.message}")
        sys.exit(1)


if __name__ == "__main__":
    main()
Run the extractor

Time to see the full pipeline in action. You will run the CLI with each schema type to verify that Claude extracts structured data correctly.

  • Run the contact extraction by entering this command in your terminal:
python main.py contact

You should see JSON output with fields like name, email, phone, company, and role populated from the sample text. At the bottom, you should see an extraction summary like 5/5 fields populated.

  • Try the invoice schema by running:
python main.py invoice
  • Try the review schema by running:
python main.py review

Each run sends the sample text to Claude, which returns structured JSON matching the Pydantic schema you defined. The extraction summary confirms how completely Claude filled in the fields.

Seeing an API error?

  • Check that your ANTHROPIC_API_KEY environment variable is still set in your current terminal session. If you opened a new terminal, you need to set it again.
  • Verify your API key is valid by checking it at console.anthropic.com under API Keys.
  • If you see a rate limit error, wait a few seconds and try again.

Help me debug an API error when running my extraction pipeline.

Your extraction pipeline is fully operational. You can now transform unstructured text into clean, validated JSON with a single terminal command. In the Secret Mission, you will extend this into a batch processor that handles entire folders of files.

Secret mission

Add Batch Processing with CSV Export

Your extraction pipeline works great for single texts. But in the real world, data arrives in batches. Can you extend your tool to process an entire folder of text files and export the results as a structured CSV report?

Clean Up Your Resources

Clean Up Your Resources

This project uses Anthropic's API on a pay-per-use basis. You are only charged when you make API calls, so there are no ongoing costs from leaving your setup in place.

Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

Resources you used:

  • Anthropic API key (pay-per-use, no recurring charge).
  • Project files on your Desktop (schemas.py, extractor.py, main.py, batch_processor.py, requirements.txt).
  • Python virtual environment (.venv folder inside your project directory).

Keep everything running

No action needed. Choose this if you plan to keep experimenting with your extraction pipeline or building on top of it.

  • Your API key remains active. You are only charged when you make API calls.
  • Monitor your usage at console.anthropic.com.
  • Your project files stay on your Desktop for future use.

Pause - I'll come back to this later

No action needed for the API. The Anthropic API does not charge when idle. Your project files stay on disk and the virtual environment stays intact.

  • Your API key stays active but costs nothing while unused.
  • When you return, reactivate the virtual environment and pick up where you left off.

Delete - I don't want to use this again

Remove all project resources and revoke API access to prevent any future usage.

  • Revoke your API key by visiting the Anthropic Console at console.anthropic.com/settings/keys. Locate the key you created for this project and disable or delete it.
  • Delete your project folder from the Desktop. On macOS or Linux, run:
cd ~/Desktop
rm -rf data-extraction-pipeline
  • On Windows, run:
cd ~\Desktop
Remove-Item -Recurse -Force data-extraction-pipeline

What about the virtual environment?

The .venv folder lives inside your project directory. Deleting the project folder removes the virtual environment and all installed packages along with it.

Nice Work!

Nice Work!

You just built a production-pattern data extraction pipeline that turns messy, unstructured text into clean, validated JSON.

This is the exact integration pattern companies use in their ETL and data processing workflows to automate information extraction at scale.

You've learned how to:

  • Build a Python CLI tool that extracts structured data from unstructured text using Claude's API.
  • Use structured outputs with Pydantic models to guarantee schema-valid JSON from an LLM.
  • Handle multiple extraction schemas (contacts, invoices, reviews) with proper error handling and environment variable management.
  • Secret Mission: Added batch processing that reads a folder of text files and exports structured results to a CSV report.

Ready to quiz yourself?