Prescription Reader with Claude Vision

Use Claude's vision API to read handwritten prescriptions into a database.

Introduction

30 Second Summary

Handwritten prescriptions are notoriously hard to read. Pharmacists and medical staff spend real time deciphering messy handwriting, and misreads can have serious consequences.

In this project, you will build a Python script that uses Claude's vision AI to read handwritten prescription images, extract structured medical data from them, and store everything in a searchable database.

What You'll Build

Point this tool at any prescription image and watch it return a clean breakdown of patient name, medication, dosage, and prescriber in seconds.

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

  • A working prescription reading pipeline that takes a handwritten prescription image, sends it to Claude's vision API, and returns structured fields like patient name, medication, dosage, and frequency.
  • A SQLite database storing every prescription you process, queryable with SQL to find specific medications or patients.
  • A clear picture of how AI vision interprets messy handwriting and returns predictable, structured data you can build on.
  • Secret Mission: A batch processor that handles multiple prescriptions at once and a search interface to find prescriptions by medication name.

Are there any prerequisites?

None. This guide walks you through setting up everything from scratch, including your code editor, Python packages, and API key. All you need is a Windows computer and an internet connection.

Before We Start

Before we dive in, let's lock in what you're about to build and why it matters.

This is your moment to commit to the goal. Once you're clear on what you're creating, every step that follows has a purpose.

Set Up Your Development Environment

To build a prescription reader, you need a code editor, the right Python packages, and access to Claude's vision API.

In this step, you will install VS Code, set up a Python project with a virtual environment, and connect to the Anthropic API.

In this step, get ready to:
  • Install VS Code and the Python extension.
  • Create a project folder with a virtual environment and install packages.
  • Set up your Anthropic API key and create the starter script.
Install VS Code and the Python extension

VS Code is a free code editor that gives you syntax highlighting, an integrated terminal, and extensions for Python development.

  • Open your browser and go to https://code.visualstudio.com.
  • Click the download button for your operating system (the site auto-detects it).
  • Run the downloaded installer and follow the prompts to complete the installation.
  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Visual Studio Code and press Enter to open it.

Now install the Python extension so VS Code understands your Python files.

  • Press Cmd+Shift+X (macOS) or Ctrl+Shift+X (Windows/Linux) to open the Extensions panel.
  • Type Python in the search box.
  • Click Install on the first result by Microsoft.

What is VS Code?

Visual Studio Code is a lightweight code editor built by Microsoft. The Python extension adds features like auto-complete, linting, and one-click running of scripts.

Create your project folder and install packages

The packages this project uses require Python 3.10 or higher. Before creating the project, confirm your Python version.

  • Open a terminal inside VS Code by pressing Cmd+` (macOS) or Ctrl+` (Windows/Linux).
  • Check your Python version by running:

macOS

python3 --version

Windows

python --version

Linux

python3 --version

✔️ I see version 3.10 or higher

You are good to go. Continue with the next instruction below.

ⓧ I see a version lower than 3.10

Your current Python is too old for the packages this project uses. Download and install the latest Python from https://www.python.org/downloads/.

macOS

  • Download the macOS universal installer for the latest Python 3 release.
  • Run the .pkg file and follow the prompts to complete the installation.
  • Close and reopen VS Code, then run python3 --version again to confirm you see 3.10 or higher.

Windows

  • Download the Windows installer (64-bit) for the latest Python 3 release.
  • Run the installer. Check the box Add python.exe to PATH on the first screen, then click Install Now.
  • Close and reopen VS Code, then run python --version again to confirm you see 3.10 or higher.

Linux

  • Use your distribution's package manager to install the latest Python 3. For Ubuntu/Debian, run:
  • sudo apt update && sudo apt install python3
  • Close and reopen VS Code, then run python3 --version again to confirm you see 3.10 or higher.

ⓧ Command not found

Python is not installed or not on your PATH. Download and install it from https://www.python.org/downloads/.

macOS

  • Download the macOS universal installer for the latest Python 3 release.
  • Run the .pkg file and follow the prompts.
  • Close and reopen VS Code, then run python3 --version to confirm you see a version number.

Windows

  • Download the Windows installer (64-bit) for the latest Python 3 release.
  • Run the installer. Check the box Add python.exe to PATH on the first screen, then click Install Now.
  • Close and reopen VS Code, then run python --version again to confirm you see a version number.

Linux

  • Use your distribution's package manager. For Ubuntu/Debian, run:
  • sudo apt update && sudo apt install python3
  • Close and reopen VS Code, then run python3 --version to confirm you see a version number.

Now create your project folder on the Desktop and move into it.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir prescription-reader
cd prescription-reader
  • Open this folder in VS Code by running:
code .

What does code . do?

The code command launches VS Code from the terminal. The dot (.) means "open the current folder." A new VS Code window opens with your prescription-reader folder loaded in the Explorer sidebar.

A new VS Code window opens with your project folder. Close the old VS Code window if one is still open.

  • Open a new terminal in this window by pressing Cmd+` (macOS) or Ctrl+` (Windows/Linux).
  • Create a virtual environment by running:

macOS

python3 -m venv .venv

Windows

python -m venv .venv

Linux

python3 -m venv .venv
  • Activate the virtual environment by running:

macOS

source .venv/bin/activate

Windows

.venv\Scripts\activate

Linux

source .venv/bin/activate

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

What is a virtual environment?

A virtual environment is an isolated Python workspace. Packages you install inside it stay separate from your system Python, preventing version conflicts between projects.

  • Install the two packages this project needs by running:
pip install anthropic==0.120.0 python-dotenv==1.2.2

You should see output ending with "Successfully installed" followed by the package names.

What are these packages?

  • anthropic is the official Python SDK for calling Claude's API.
  • python-dotenv loads environment variables from a .env file so you can keep secrets out of your code.

🙋‍♀️ Seeing a pip error?

Make sure you see (.venv) in your terminal prompt. If you do not, the virtual environment is not active. Re-run the activation command for your platform (source .venv/bin/activate on macOS/Linux or .venv\Scripts\activate on Windows).

If you see a permissions error on macOS/Linux, try pip install --user instead. On Windows, try running the install command again without sudo.

Still stuck? Help me fix my pip install error.

Set up your Anthropic API key and create the starter script

Claude's vision API requires an API key to authenticate your requests. You will create an account, grab a key, then build the starter script that proves everything is wired up.

  • Open your browser and go to https://console.anthropic.com.
  • Click Sign up and create an account (you can use Google or email).
  • Once signed in, navigate to API Keys in the left sidebar.
  • Click Create Key. Give it a name like prescription-reader and copy the key immediately.

Save your key now

Anthropic only shows the full key once. Copy it to a safe place before closing the dialog. You will paste it into a file in the next step.

Now create the starter file that loads your API key and confirms the setup works.

  • In VS Code, create a new file by pressing Cmd+N (macOS) or Ctrl+N (Windows/Linux), then save it as main.py with Cmd+S (macOS) or Ctrl+S (Windows/Linux).
  • Paste the following code into main.py:
import os
import sys
import json
import base64
import sqlite3
from datetime import datetime

from dotenv import load_dotenv
from anthropic import Anthropic

# Load environment variables from .env file
load_dotenv()

# Create the Anthropic API client
client = Anthropic()

# Database file path
DATABASE = "prescriptions.db"


if __name__ == "__main__":
    if not os.environ.get("ANTHROPIC_API_KEY"):
        print("Setup complete! Ready to read prescriptions.")
        print("(Set your ANTHROPIC_API_KEY in .env to process images)")
    else:
        main()

What does this code do?

  • The imports at the top bring in every module you will use across the project (image encoding, JSON parsing, database access, and the Anthropic SDK).
  • load_dotenv() reads your .env file and loads its values into environment variables.
  • The if __name__ block checks whether a key is set. If not, it prints a confirmation message so you know the setup succeeded.

Before you run this, think about what you expect to see. You have not created a .env file yet, so ANTHROPIC_API_KEY is not set. What will the script print?

  • Save main.py with Cmd+S (macOS) or Ctrl+S (Windows/Linux).
  • Run the script by entering this in your terminal:
python main.py

You should see:

Setup complete! Ready to read prescriptions.
(Set your ANTHROPIC_API_KEY in .env to process images)

Seeing a ModuleNotFoundError?

This means your virtual environment is not active. Check that (.venv) appears in your terminal prompt. If it does not, re-run the activation command for your platform (source .venv/bin/activate on macOS/Linux or .venv\Scripts\activate on Windows) and try again.

If you see an error about anthropic or dotenv, re-run the pip install command from the previous substep.

Still stuck? Help me fix my ModuleNotFoundError in Python.

Now create the .env file to store your API key for future steps.

  • Create a new file in VS Code with Cmd+N (macOS) or Ctrl+N (Windows/Linux) and save it as .env.
  • Paste the following line, replacing your-api-key-here with the API key you copied from the Anthropic console:
ANTHROPIC_API_KEY=your-api-key-here
  • Save the file with Cmd+S (macOS) or Ctrl+S (Windows/Linux).

Why use a .env file?

Storing secrets in a .env file keeps them out of your code. If you ever share your project or push it to GitHub, the .env file stays private (as long as you add it to .gitignore).

✔️ Awesome, I've got everything!

Great. Make sure you have saved both main.py and .env.

ⓧ I'd like to double check the full code

Here is main.py in full:

import os
import sys
import json
import base64
import sqlite3
from datetime import datetime

from dotenv import load_dotenv
from anthropic import Anthropic

load_dotenv()

client = Anthropic()

DATABASE = "prescriptions.db"


if __name__ == "__main__":
    if not os.environ.get("ANTHROPIC_API_KEY"):
        print("Setup complete! Ready to read prescriptions.")
        print("(Set your ANTHROPIC_API_KEY in .env to process images)")
    else:
        main()

And here is .env:

ANTHROPIC_API_KEY=your-api-key-here

Your development environment is ready. You have VS Code, a virtual environment with the packages installed, and an API key connected. Next up, you will download sample prescription files and send them to Claude's vision API to see AI read them in real time.

Create a Sample Prescription and Read It with Claude

Read PDF/PNG Files with Claude (e.g. Prescription Files)

Your environment is ready to go. Now you need files to actually process.

In this step, you will download sample prescription files from GitHub and write code that sends them to Claude's vision API. Claude reads both PDF and PNG formats, so you can process prescriptions in whichever format you have.

In this step, get ready to:
  • Download sample prescription files from GitHub.
  • Write code to send PDF and PNG files to Claude's vision API.
  • Run the script and see Claude read your prescriptions.
Download the sample prescription files

You have a GitHub repository with six sample prescriptions: three PNGs (scanned handwritten) and three PDFs. Each has different doctors, patients, and medications.

  • Clone the sample prescriptions into your project folder by running this command:
git clone https://github.com/gjoshius/prescription-samples.git prescriptions
  • Confirm the files are there by running:
ls prescriptions/png
ls prescriptions/pdf

You should see three .png files and three .pdf files listed.

What's in this repo?

The repository contains six unique prescriptions from different doctors. The PNGs are scanned handwritten prescriptions (single medication, two medications, and a complex three-medication prescription). The PDFs cover pediatric, post-surgery, and elderly patient scenarios.

You can also create your own prescriptions using the editable .docx template in the template/ folder. Export as PDF or PNG and drop it into the same folder.

🙋‍♀️ git clone not working?

If you see fatal: destination path 'prescriptions' already exists, delete the existing folder first. On macOS/Linux, run rm -rf prescriptions. On Windows, run rmdir /s /q prescriptions. Then try the clone command again.

If you see a network error, check your internet connection. You can also download the files manually from https://github.com/gjoshius/prescription-samples and place them in a prescriptions/ folder.

help me clone the prescription samples repo

Send files to Claude's vision API

Now you will write the code that reads each file, converts it to a format Claude can understand, and sends it to the vision API. The code handles both PNG images and PDF documents.

  • In main.py, find the line that says DATABASE = "prescriptions.db". Add the following two functions below it:
def read_file(file_path):
    """Read a file and return base64-encoded data."""
    with open(file_path, "rb") as f:
        return base64.standard_b64encode(f.read()).decode("utf-8")

def process_prescription(file_path):
    """Send a single prescription file to Claude and print the result."""
    ext = os.path.splitext(file_path)[1].lower()
    media_types = {".png": "image/png", ".jpg": "image/jpeg", ".pdf": "application/pdf"}
    media_type = media_types.get(ext, "image/png")
    file_data = read_file(file_path)

    # PDFs use "document" type, images use "image" type
    block_type = "document" if ext == ".pdf" else "image"
    content_block = {
        "type": block_type,
        "source": {"type": "base64", "media_type": media_type, "data": file_data},
    }

    # Send to Claude's vision API
    response = client.messages.create(
        model="claude-sonnet-4-5-20250929",
        max_tokens=1024,
        messages=[{"role": "user", "content": [
            content_block,
            {"type": "text", "text": "Read this prescription. List all information you can extract."},
        ]}],
    )
    print(f"\n--- {os.path.basename(file_path)} ---")
    print(response.content[0].text)

What does this code do?

  • read_file() opens any file in binary mode and converts it to a base64-encoded string. This is how you embed file data inside a JSON request.
  • process_prescription() detects the file type, builds the right content block ("document" for PDFs, "image" for PNGs), and sends it to Claude with a prompt asking it to extract all information.
  • Claude's API requires different block types for different formats. PDFs use "type": "document" while images use "type": "image".
  • Add the main() function below process_prescription. This finds all files and processes them:
def main():
    """Find all prescription files and process them with Claude."""
    prescriptions_dir = "prescriptions"
    supported_ext = (".png", ".jpg", ".jpeg", ".pdf")

    # Walk through all subfolders to find supported files
    files = []
    for root, dirs, filenames in os.walk(prescriptions_dir):
        for filename in filenames:
            if filename.lower().endswith(supported_ext):
                files.append(os.path.join(root, filename))

    if not files:
        print("No prescription files found in prescriptions/ folder.")
        sys.exit(1)

    print(f"Found {len(files)} prescription file(s). Processing...\n")
    for file_path in sorted(files):
        process_prescription(file_path)

What does main() do?

os.walk() searches through the prescriptions/ folder and all its subfolders (like png/ and pdf/) for files ending in .png, .jpg, or .pdf. It processes each one in alphabetical order, so you see results for all six prescriptions.

  • Save main.py.

✔️ Awesome, I've got everything!

Great. Make sure you have saved the file before running it.

ⓧ I'd like to double check the full code

Scroll through main.py and confirm these sections are present, in order:

  • Imports (os, sys, json, base64, sqlite3, datetime, dotenv, Anthropic).
  • Global setup: load_dotenv(), client = Anthropic(), DATABASE = "prescriptions.db".
  • The read_file function.
  • The process_prescription function.
  • The main() function.
  • The if __name__ == "__main__" block (unchanged from Step 1).

Before you run this, what do you think Claude will return for each file? A single sentence? A structured list? Something different for PDFs versus PNGs?

  • Run the script to send all prescriptions to Claude:
python main.py

You should see Claude's interpretation of each prescription printed one after another, separated by the filename.

What should I see?

Claude returns a text description for each file. For each prescription, it identifies the doctor, patient, medications, dosages, and instructions. The exact wording varies each time because Claude generates a fresh response.

The key proof is that Claude correctly reads both PNG images and PDF documents. This raw text is what you will structure into specific fields in the next step.

🙋‍♀️ Seeing an authentication error?

If you see an error mentioning your API key, open your .env file and confirm ANTHROPIC_API_KEY is set to your actual key from the Anthropic console. Make sure there are no extra spaces or quotes around the key value.

If you see No prescription files found, make sure the git clone command created a prescriptions/ folder in the same directory as main.py. Run ls prescriptions to check.

help me fix my prescription reader errors

You have proven that Claude's vision API can read both PDF and PNG prescription files and return a text description. Next up, you will make Claude return that data in a structured format you can actually parse and store.

Extract Structured Data from the Prescription

In the previous step, Claude returned a wall of text describing the prescription. That text is useful to read, but you cannot search it or query it programmatically.

To answer questions like "what medication was prescribed?" you need Claude to return specific, predictable fields. In this step, you will upgrade your prompt so Claude returns structured JSON data, then parse that response into a clean Python dictionary.

In this step, get ready to:
  • Modify the prompt so Claude returns a JSON object with specific medical fields.
  • Add JSON parsing to extract a structured dictionary from the response.
  • Add error handling that prints the raw response when parsing fails.
Upgrade the prompt to request structured JSON

Right now, your prompt just says "Read this handwritten prescription" and Claude responds with freeform text. You need it to return a JSON object with exact keys you can parse.

  • In main.py, find the text field inside your messages list:
                    {
                        "type": "text",
                        "text": "Read this handwritten prescription",
                    },
  • Replace that entire block with this structured prompt:
                    {
                        "type": "text",
                        "text": (
                            "Read this handwritten prescription image and extract the "
                            "following fields. Return ONLY a JSON object with these keys:\n"
                            "- patient_name\n"
                            "- prescriber_name\n"
                            "- date\n"
                            "- medication\n"
                            "- dosage\n"
                            "- frequency\n"
                            "- refills\n"
                            "- notes (any additional instructions)\n\n"
                            "If a field is not visible, use null. Return only the JSON, "
                            "no other text."
                        ),
                    },

What does this prompt do?

The prompt explicitly lists the keys you want in the JSON output. It also tells Claude to use null for missing fields and to return nothing but JSON.

This constrained prompt style is called structured extraction. It turns freeform AI output into predictable data you can parse with code.

  • Save main.py and run it to see the updated response:
python main.py

You should see a JSON object printed in the terminal with keys like patient_name, medication, and dosage instead of a paragraph of text.

Still seeing a paragraph of text?

Make sure you replaced the entire text field, including the curly braces and the "type": "text" line. The new prompt must be inside the same position in the content list.

Also confirm you saved the file before running. Help me debug why Claude is not returning JSON.

Parse the JSON response and refactor into a function

Claude is now returning JSON text, but your code still just prints it as a raw string. You need to parse that string into a Python dictionary so you can access individual fields like data["medication"].

  • In main.py, replace the entire def main(): function (everything from def main(): down to the print(response.content[0].text) line) with a new extract_prescription_data function. Copy this code in its place:
def extract_prescription_data(image_path):
    """Send a prescription image to Claude and get structured data back."""
    image_data = read_prescription_image(image_path)

    # Determine media type from file extension
    ext = os.path.splitext(image_path)[1].lower()
    media_types = {".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg"}
    media_type = media_types.get(ext, "image/png")

    response = client.messages.create(
        model="claude-sonnet-4-5-20250929",
        max_tokens=1024,
        messages=[
            {
                "role": "user",
                "content": [
                    {
                        "type": "image",
                        "source": {
                            "type": "base64",
                            "media_type": media_type,
                            "data": image_data,
                        },
                    },
  • Directly below that code (still inside the messages list), add the structured prompt and the JSON parsing logic:
                    {
                        "type": "text",
                        "text": (
                            "Read this handwritten prescription image and extract the "
                            "following fields. Return ONLY a JSON object with these keys:\n"
                            "- patient_name\n"
                            "- prescriber_name\n"
                            "- date\n"
                            "- medication\n"
                            "- dosage\n"
                            "- frequency\n"
                            "- refills\n"
                            "- notes (any additional instructions)\n\n"
                            "If a field is not visible, use null. Return only the JSON, "
                            "no other text."
                        ),
                    },
                ],
            }
        ],
    )

    response_text = response.content[0].text

Why extract into a function?

Wrapping the API call in extract_prescription_data(image_path) lets you reuse it later on any image file. The function takes a file path and returns structured data.

Add error handling for failed JSON parsing

Claude sometimes wraps its JSON response in markdown code blocks (triple backticks). Your parser needs to strip those before calling json.loads(). If parsing still fails, you want to see the raw response for debugging.

  • Directly below the response_text = response.content[0].text line you just added, paste this parsing and error handling block:
    # Try to parse JSON from the response
    try:
        # Handle case where response is wrapped in markdown code block
        cleaned = response_text.strip()
        if cleaned.startswith("```"):
            cleaned = cleaned.split("\n", 1)[1]
            cleaned = cleaned.rsplit("```", 1)[0]
        data = json.loads(cleaned)
    except json.JSONDecodeError:
        print(f"Warning: Could not parse JSON from response for {image_path}")
        print(f"Raw response:\n{response_text}")
        return None

    return data

What does this parsing code do?

  • It strips leading/trailing whitespace from the response.
  • If the response starts with ```, it removes the first line (the opening fence) and everything after the closing fence.
  • json.loads() converts the cleaned string into a Python dictionary.
  • If parsing fails, it prints the raw response so you can see exactly what Claude returned and debug the issue.
  • Now add a new main() function below extract_prescription_data that calls it and prints the results:
def main():
    sample = os.path.join("prescriptions", "prescription_01.png")
    data = extract_prescription_data(sample)
    if data:
        print("\nExtracted data:")
        for key, value in data.items():
            print(f"  {key}: {value}")

What does the new main() do?

It points at your sample prescription image, calls extract_prescription_data() to get a dictionary back, then prints each key-value pair on its own line. This gives you a clean, readable breakdown of the prescription.

  • Save main.py.

Before you run this, what do you expect to see? A paragraph of text, raw JSON, or a labelled key-value list?

  • Run your updated script:
python main.py

You should see output like this, with each prescription field on its own line:

Extracted data:
  patient_name: John Smith
  prescriber_name: Dr. Sarah Johnson
  date: 07/25/2026
  medication: Amoxicillin 500mg
  dosage: 500mg
  frequency: 1 capsule 3 times daily for 10 days
  refills: 2
  notes: Ibuprofen 400mg - Take 1 tablet as needed for pain

Your output may differ slightly

Claude's interpretation varies between runs. The exact values might be phrased slightly differently, but you should see all 8 keys with reasonable values extracted from the prescription image.

🙋‍♀️ Seeing a JSONDecodeError warning?

Check that your structured prompt is the only change inside the text field. If the raw response is printed, look at it to see what Claude actually returned. It might be a partial JSON or have extra text before the opening brace.

Also confirm you kept the json import at the top of your file (it was added in Step 1). Help me debug my JSON parsing error.

✔️ Awesome, I've got everything!

Great. Double check you have saved main.py and that the output shows all 8 keys.

ⓧ I'd like to double check the full code

Scroll up and confirm your main.py has these sections in order:

  • Imports (os, sys, json, base64, sqlite3, datetime, dotenv, anthropic).
  • load_dotenv(), client = Anthropic(), and DATABASE = "prescriptions.db".
  • read_prescription_image(image_path) function (unchanged from Step 2).
  • extract_prescription_data(image_path) function with the structured prompt, JSON parsing, and error handling.
  • main() function that calls extract_prescription_data and prints key-value pairs.
  • The if __name__ == "__main__": block with the API key check (unchanged).

Your script now takes a messy handwritten image and returns clean, labelled data. Next up, you will store that data in a database so it persists between runs and becomes searchable.

Store in SQLite and Query Your Data

You can now extract structured data from a prescription image. The problem is that this data lives only in your terminal output.

The moment you close the terminal, everything disappears. A database makes prescriptions permanent, searchable, and queryable so you can answer questions across every prescription you process.

In this step, get ready to:
  • Create a SQLite database with a prescriptions table.
  • Insert extracted prescription data into the database.
  • Query and display stored records in a formatted table.
Create the prescriptions table

Python includes SQLite in its standard library. No extra install needed.

  • In main.py, find the line DATABASE = "prescriptions.db". Add the following function directly below it (before read_prescription_image):
def init_database():
    """Create the prescriptions table if it does not exist."""
    conn = sqlite3.connect(DATABASE)
    cursor = conn.cursor()
    cursor.execute("""
        CREATE TABLE IF NOT EXISTS prescriptions (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            patient_name TEXT,
            prescriber_name TEXT,
            date TEXT,
            medication TEXT,
            dosage TEXT,
            frequency TEXT,
            refills TEXT,
            notes TEXT,
            source_file TEXT,
            extracted_at TEXT
        )
    """)
    conn.commit()
    conn.close()

What does this code do?

This function creates the database file and a table to hold prescription records.

  • sqlite3.connect(DATABASE) opens (or creates) the prescriptions.db file.
  • CREATE TABLE IF NOT EXISTS ensures the table is only created once, even if you run the script multiple times.
  • Each column maps to one field that Claude extracts from the prescription image.
  • Now add the store_prescription function below extract_prescription_data. Copy the following code after that function:
def store_prescription(data, source_file):
    """Store extracted prescription data in SQLite."""
    conn = sqlite3.connect(DATABASE)
    cursor = conn.cursor()
    cursor.execute(
        """
        INSERT INTO prescriptions
            (patient_name, prescriber_name, date, medication, dosage,
             frequency, refills, notes, source_file, extracted_at)
        VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
    """,
        (
            data.get("patient_name"),
            data.get("prescriber_name"),
            data.get("date"),
            data.get("medication"),
            data.get("dosage"),
            data.get("frequency"),
            data.get("refills"),
            data.get("notes"),
            source_file,
            datetime.now().isoformat(),
        ),
    )
    conn.commit()
    conn.close()
    print(f"Stored prescription for {data.get('patient_name')} in database.")

What does this code do?

This function takes the dictionary that Claude returned and inserts each field into the database as a new row.

  • The ? placeholders prevent SQL injection by letting SQLite handle value escaping.
  • data.get("patient_name") safely returns None if a key is missing rather than crashing.
  • source_file and extracted_at track which image was processed and when.
  • Now update your existing main() function to call these new functions. Find your current main() and replace it with:
def main():
    init_database()

    sample = os.path.join("prescriptions", "prescription_01.png")
    data = extract_prescription_data(sample)
    if data:
        print("\nExtracted data:")
        for key, value in data.items():
            print(f"  {key}: {value}")
        store_prescription(data, sample)

What changed in main()?

  • init_database() now runs first to ensure the table exists before any data is inserted.
  • store_prescription(data, sample) saves the extracted fields to the database after printing them.
  • Save main.py.
  • Before you run this, what do you think will appear in the terminal? Will you see anything new beyond the extracted data?
  • Run the script to test storage by running:
python main.py

You should see the familiar extracted data printed, followed by a new line: Stored prescription for John Smith in database.

  • Confirm the database file was created by running:
dir prescriptions.db

You should see prescriptions.db listed with a non-zero file size.

Not seeing the database file?

  • Make sure init_database() is the first line inside your main() function. Without it, the table does not exist and the insert will fail.
  • Check that import sqlite3 and from datetime import datetime are both at the top of your file.
  • help me debug why my database file is not being created
Display stored prescriptions

Data is in the database, but you cannot see it without a query. The next function reads all records and prints them as a formatted table.

  • Add the query_all function below store_prescription in main.py:
def query_all():
    """Display all prescriptions in the database."""
    conn = sqlite3.connect(DATABASE)
    conn.row_factory = sqlite3.Row
    cursor = conn.cursor()
    cursor.execute("SELECT * FROM prescriptions")
    rows = cursor.fetchall()
    conn.close()

    if not rows:
        print("No prescriptions in database.")
        return

    print(f"\n{'='*60}")
    print(f"{'PRESCRIPTIONS DATABASE':^60}")
    print(f"{'='*60}")
    for row in rows:
        print(f"\n  Patient: {row['patient_name']}")
        print(f"  Prescriber: {row['prescriber_name']}")
        print(f"  Date: {row['date']}")
        print(f"  Medication: {row['medication']}")
        print(f"  Dosage: {row['dosage']}")
        print(f"  Frequency: {row['frequency']}")
        print(f"  Refills: {row['refills']}")
        print(f"  Notes: {row['notes']}")
        print(f"  Source: {row['source_file']}")
        print(f"  {'-'*56}")
    print(f"\nTotal: {len(rows)} prescription(s)")

What does this code do?

  • conn.row_factory = sqlite3.Row lets you access columns by name (like row['patient_name']) instead of numeric index.
  • The formatted print statements create a clean, readable table so you can quickly scan stored prescriptions.
  • Add a query_all() call at the end of your main() function. Find this line inside main():
        store_prescription(data, sample)
  • Add two lines directly after it so the end of main() looks like this:
        store_prescription(data, sample)
    print("\n")
    query_all()
  • Save main.py and run it again:
python main.py

You should see the extracted data, the storage confirmation, and then a formatted table showing your stored prescription with all fields.

Seeing duplicate records in the table?

Each run inserts a new row. If you ran the script multiple times, you will see duplicates. Delete the database file and re-run to start fresh:

del prescriptions.db
python main.py

help me understand why I have duplicate prescriptions in my database

Add search, batch processing, and CLI flags

A single query is useful, but a real tool needs to search by medication name and process multiple files. These final functions complete the pipeline.

  • Add the search_by_medication function below query_all in main.py:
def search_by_medication(medication_name):
    """Search prescriptions by medication name."""
    conn = sqlite3.connect(DATABASE)
    conn.row_factory = sqlite3.Row
    cursor = conn.cursor()
    cursor.execute(
        "SELECT * FROM prescriptions WHERE medication LIKE ?",
        (f"%{medication_name}%",),
    )
    rows = cursor.fetchall()
    conn.close()

    if not rows:
        print(f"No prescriptions found containing '{medication_name}'.")
        return

    print(f"\nSearch results for '{medication_name}':")
    for row in rows:
        print(f"  - {row['patient_name']}: {row['medication']} ({row['dosage']})")

What does LIKE do?

The LIKE operator with % wildcards performs a partial match. Searching for "amox" finds "Amoxicillin 500mg" because the wildcards match any text before and after.

  • Add the process_single and process_batch helper functions below search_by_medication. First, add process_single:
def process_single(image_path):
    """Process a single prescription image."""
    print(f"\nProcessing: {image_path}")
    data = extract_prescription_data(image_path)
    if data:
        print("\nExtracted data:")
        for key, value in data.items():
            print(f"  {key}: {value}")
        store_prescription(data, image_path)
    return data
  • Add process_batch directly below process_single:
def process_batch(folder="prescriptions"):
    """Process all PNG files in a folder."""
    if not os.path.exists(folder):
        print(f"Folder '{folder}' not found.")
        return

    files = [f for f in os.listdir(folder) if f.lower().endswith((".png", ".jpg", ".jpeg"))]
    if not files:
        print(f"No image files found in '{folder}'.")
        return

    print(f"\nBatch processing {len(files)} prescription(s)...")
    successes = 0
    failures = []

    for filename in files:
        filepath = os.path.join(folder, filename)
        try:
            data = process_single(filepath)
            if data:
                successes += 1
            else:
                failures.append(filename)
        except Exception as e:
            print(f"  Error processing {filename}: {e}")
            failures.append(filename)

    print(f"\nBatch complete: {successes} succeeded, {len(failures)} failed.")
    if failures:
        print(f"Failed files: {', '.join(failures)}")

What do these helpers do?

  • process_single wraps the extract-then-store logic into one call so you do not repeat yourself.
  • process_batch loops through every image in a folder, processes each one, and reports successes and failures.
  • The try/except block means one bad file does not crash the entire batch.
  • Finally, replace your current main() function with this complete version that supports command-line flags:
def main():
    init_database()

    if len(sys.argv) < 2:
        # Default: process single sample prescription
        sample = os.path.join("prescriptions", "prescription_01.png")
        if not os.path.exists(sample):
            print("No sample found. Run create_sample.py first.")
            print("  python create_sample.py")
            return
        process_single(sample)
        print("\n")
        query_all()

    elif sys.argv[1] == "--batch":
        process_batch()
        print("\n")
        query_all()

    elif sys.argv[1] == "--search" and len(sys.argv) > 2:
        search_by_medication(sys.argv[2])

    elif sys.argv[1] == "--query":
        query_all()

    else:
        print("Usage:")
        print("  python main.py              Process sample prescription")
        print("  python main.py --batch      Process all images in prescriptions/")
        print("  python main.py --search <med>  Search by medication name")
        print("  python main.py --query      Show all stored prescriptions")

How do the CLI flags work?

  • No arguments: processes the sample prescription, stores it, and displays all records.
  • --batch: processes every image in the prescriptions/ folder.
  • --search: finds prescriptions matching a medication name.
  • --query: displays all stored prescriptions without processing new ones.
  • Save main.py.
  • Delete the old database so you start fresh, then run the completed script:
del prescriptions.db
python main.py

What do you expect to see? You should see the prescription processed, stored in the database, and then a formatted table displaying the full record with patient name, medication, dosage, and all other fields.

  • Now test the search flag by running:
python main.py --search amoxicillin

You should see a search result showing John Smith's Amoxicillin prescription with its dosage.

Seeing "No prescriptions found"?

The search is case-insensitive for SQLite by default with ASCII characters. If your search term does not match, check that the prescription was stored by running:

python main.py --query

If no records appear, the database may have been deleted or the previous run failed. Run python main.py once (no flags) to process and store the prescription first.

help me debug why my prescription search is returning no results

✔️ Awesome, I've got everything!

Great. Double check you have saved main.py and that python main.py --query displays your stored prescription.

ⓧ I'd like to double check the full code

Your main.py is now quite long. Scroll through your file and confirm you have these functions in order:

  • init_database()
  • read_prescription_image()
  • extract_prescription_data()
  • store_prescription()
  • query_all()
  • search_by_medication()
  • process_single()
  • process_batch()
  • main()

The if __name__ == "__main__": block at the bottom should remain unchanged from the previous step. It checks for the API key and either prints a setup message or calls main().

Your prescription reader is now a complete pipeline. You feed in a handwritten image, AI extracts the data, and a database stores it permanently. Next up, the Secret Mission challenges you to scale this to multiple prescriptions at once.

Secret mission

Batch Process Multiple Prescriptions

Your pipeline handles one prescription. But a real clinic has dozens. Can you scale it to process an entire folder of prescriptions at once and search across all of them by medication name?

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally, so there are no ongoing costs. The only charges you may have incurred are a few cents of Anthropic API usage, which is covered by free credits.

Decide whether to keep your resources on your machine, or delete them entirely.

Resources you used:

  • Project folder on your Desktop (contains main.py, create_sample.py, .env, prescriptions.db, prescriptions/ folder, and .venv virtual environment).
  • Anthropic API key (created at console.anthropic.com).

Keep everything running

No action needed. Choose this if you plan to keep experimenting with your prescription reader or want to process more images later.

  • Your project folder stays on your Desktop. No ongoing costs unless you call the API again.
  • The prescriptions.db database stays on disk and you can query it any time with python main.py --query.
  • Your Anthropic API key remains active but incurs no charges while idle.

Pause - I'll come back to this later

Nothing to pause. The API key can sit unused with no charges, and the local files use negligible disk space.

  • Deactivate your virtual environment by running:
  • Your files remain on disk, ready to pick up again whenever you return.
deactivate

Delete - I don't want to use this again

Remove all project files and revoke your API key so nothing remains.

  • Deactivate the virtual environment first by running:
deactivate
  • Delete the project folder. On Windows, run:
cd ~\Desktop
rmdir /s /q prescription-reader
  • On macOS or Linux, run:
cd ~/Desktop
rm -rf prescription-reader
  • To revoke your Anthropic API key, sign in to the Anthropic Console at console.anthropic.com, navigate to your API keys settings, and delete the key you created for this project.

Nice Work!

Nice Work!

You just built an AI-powered prescription reader that turns messy handwritten images into structured, queryable database records. That is a real document-processing pipeline, the kind of tool that saves hours of manual data entry in clinics and pharmacies.

You learned how to:

  • Built a Python script that reads handwritten prescription images using Claude's vision API and base64 image encoding.
  • Engineered prompts that extract structured JSON data from unstructured handwriting.
  • Designed a SQLite database schema and stored AI-extracted data in it.
  • Queried your database with SQL to search and filter prescription records.
  • Secret Mission: Built a batch processor that handles multiple prescriptions and a search interface to find prescriptions by medication.

Ready to quiz yourself?