extract data from PDF pythonpdf data extraction pythonpython pdf parser

How to Extract Data from PDF with Python: A Complete Developer Guide (2026)

March 16, 2026 · Updated

The Problem with Raw PDF Extraction in Python

Every Python developer eventually hits the PDF wall. You've got invoices, bank statements, or contracts to process. You reach for PyPDF2 or pdfplumber. You get text. A wall of text — no structure, no field boundaries, no way to reliably pull out the vendor name or invoice total without writing brittle regex that breaks on the next slightly-different PDF.

This guide walks through four approaches to PDF data extraction in Python, from the most basic to the most production-ready, with real code for each.

Approach 1: PyPDF2 — Basic Text Extraction

PyPDF2 is the simplest entry point. It extracts text page by page, but gives you no awareness of layout, tables, or field structure.

import PyPDF2

def extract_text_pypdf2(pdf_path: str) -> str:
    with open(pdf_path, 'rb') as f:
        reader = PyPDF2.PdfReader(f)
        text = ''
        for page in reader.pages:
            text += page.extract_text() + '\n'
    return text

# What you get back:
# "INVOICE\nInvoice #: INV-2026-0042\nDate: March 15, 2026\nBill To: Acme Corp..."

Problems: No structure. You're left parsing a string. Multi-column PDFs produce garbled output. Scanned PDFs return empty strings (no OCR).

Approach 2: pdfplumber — Layout-Aware Extraction

pdfplumber understands layout better than PyPDF2. It can extract tables and has better positional awareness.

import pdfplumber

def extract_with_pdfplumber(pdf_path: str):
    with pdfplumber.open(pdf_path) as pdf:
        for page in pdf.pages:
            # Extract tables if present
            tables = page.extract_tables()
            for table in tables:
                for row in table:
                    print(row)  # Each row is a list of cell values
            
            # Extract full text
            text = page.extract_text()
            print(text)

Better, but still: You have to write table parsing logic per document type. Scanned PDFs still return nothing. Different invoice templates from different vendors break your parser.

Approach 3: AWS Textract via boto3

AWS Textract handles scanned documents and can extract forms and tables. But the setup is significant.

import boto3
import json

textract = boto3.client('textract', region_name='us-east-1')

def extract_with_textract(pdf_path: str):
    with open(pdf_path, 'rb') as f:
        response = textract.analyze_document(
            Document={'Bytes': f.read()},
            FeatureTypes=['FORMS', 'TABLES']
        )
    
    # Extract key-value pairs from forms
    key_map = {}
    value_map = {}
    block_map = {}
    
    for block in response['Blocks']:
        block_map[block['Id']] = block
        if block['BlockType'] == 'KEY_VALUE_SET':
            if 'KEY' in block.get('EntityTypes', []):
                key_map[block['Id']] = block
            else:
                value_map[block['Id']] = block
    
    # ... 40 more lines to reconstruct key-value pairs
    # ... then you still need to map Textract's keys to your field names
    return key_map, value_map

The real cost: AWS account required, IAM credentials to configure, $1.50 per 1,000 pages for form/table extraction, and you still have to write post-processing to map Textract's generic KEY_VALUE_SET blocks to your actual fields (vendor_name, invoice_total, due_date, etc.).

Approach 4: Schema-First Extraction with Dokyumi

Dokyumi lets you define the fields you want and returns extracted data with confidence and validation details. It accepts supported scanned or digital PDF, JPEG, PNG, TIFF, and WEBP inputs. No AWS account. Free tier: 25 credits/month; one credit covers a document up to five pages.

Step 1: Define your schema (one time, in the dashboard)

Go to dokyumi.com/dashboard, create a schema for "Invoice" with fields: vendor_name, invoice_number, invoice_date, due_date, subtotal, tax_amount, total_amount, line_items (array).

Or let AI infer the schema for you by describing the document type in plain English.

Your schema gets a slug (for example, invoice-extractor). Send that slug in the multipart schema field to the shared extraction endpoint.

Step 2: Extract a PDF with Python

import requests
import json
from pathlib import Path

DOKYUMI_API_KEY = "dk_live_your_key_here"
SCHEMA_SLUG = "invoice-extractor"

def extract_invoice(pdf_path: str) -> dict:
    with open(pdf_path, 'rb') as f:
        response = requests.post(
            "https://dokyumi.com/api/v1/extract",
            headers={"Authorization": f"Bearer {DOKYUMI_API_KEY}"},
            files={"file": (Path(pdf_path).name, f, "application/pdf")},
            data={"schema": SCHEMA_SLUG},
        )
    response.raise_for_status()
    return response.json()

# What you get back:
result = extract_invoice("vendor-invoice.pdf")
# {
#   "id": "6f8c2d4a-7b31-4e95-9a20-c1d7f6b84210",
#   "status": "completed",
#   "schema": "invoice-extractor",
#   "data": {
#     "vendor_name": "Acme Supplies Inc.",
#     "invoice_number": "INV-2026-0042",
#     "total_amount": 4632.50
#   },
#   "confidence": {"vendor_name": 0.98, "invoice_number": 0.96, "total_amount": 0.94},
#   "validation": {"valid": True, "errors": [], "low_confidence_fields": []},
#   "meta": {
#     "processing_time_ms": 1843,
#     "page_count": 1,
#     "credits_used": 1,
#     "ocr_cached": False,
#     "model": "anthropic/claude-sonnet-4"
#   },
#   "request_id": "5a1d9e73-4c26-48b0-a915-7f3e2c6d8041"
# }

Step 3: Handle confidence and routing

def process_invoice(pdf_path: str) -> dict:
    result = extract_invoice(pdf_path)
    low_confidence = result["validation"]["low_confidence_fields"]
    validation_errors = result["validation"]["errors"]

    if result["status"] == "completed":
        return {
            "action": "auto_approve",
            "data": result["data"],
            "extraction_id": result["id"],
        }

    if result["status"] == "review":
        return {
            "action": "review",
            "data": result["data"],
            "low_confidence_fields": low_confidence,
            "validation_errors": validation_errors,
            "extraction_id": result["id"],
        }

    raise ValueError(f"Unexpected successful response status: {result['status']}")

Step 4: Batch processing multiple PDFs

import concurrent.futures
from pathlib import Path

def batch_extract(pdf_directory: str, max_workers: int = 5) -> list:
    pdf_files = list(Path(pdf_directory).glob("*.pdf"))
    results = []
    
    with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
        futures = {
            executor.submit(extract_invoice, str(pdf)): pdf 
            for pdf in pdf_files
        }
        for future in concurrent.futures.as_completed(futures):
            pdf_file = futures[future]
            try:
                result = future.result()
                results.append({"file": pdf_file.name, "result": result})
            except Exception as e:
                results.append({"file": pdf_file.name, "error": str(e)})
    
    return results

Step 5: Webhook delivery for upload-site submissions

The direct extraction API returns synchronously and does not accept a per-request webhook URL. Webhooks apply only to documents submitted through an upload site whose URL is configured under Sites → Settings. The example below assumes support has provisioned a signing secret for that site; if the signature header is empty, provision or rotate the secret before relying on verification.

from flask import Flask, request, jsonify
import hashlib
import hmac
import re

app = Flask(__name__)
WEBHOOK_SECRET = "your_webhook_secret"

@app.route("/webhooks/dokyumi", methods=["POST"])
def handle_extraction_complete():
    signature = request.headers.get("X-Dokyumi-Signature", "")
    raw_body = request.get_data()
    expected = hmac.new(
        WEBHOOK_SECRET.encode(), raw_body, hashlib.sha256
    ).hexdigest()

    if not re.fullmatch(r"[0-9a-f]{64}", signature) or not hmac.compare_digest(
        expected, signature
    ):
        return jsonify({"error": "Invalid signature"}), 401

    event = request.get_json()
    if event["event"] != "extraction.completed":
        return jsonify({"error": "Unsupported event"}), 400

    # Upsert durably using extraction_id before acknowledging the delivery.
    save_to_database(
        extraction_id=event["extraction_id"],
        data=event["data"],
    )
    return jsonify({"received": True})

Handling Edge Cases

Scanned PDFs and low-quality images

Dokyumi's Mistral OCR layer handles scanned documents automatically. No code change needed on your end. For best results: 150+ DPI, avoid extreme skew, ensure good contrast. The confidence score will reflect OCR quality — route low-confidence results to manual review.

Multi-page documents

Multi-page PDFs are handled as a single document. Fields are extracted across all pages. Line items from a 10-page invoice are consolidated into a single line_items array.

International invoices

Include date format and currency in your schema description: "Extract dates in ISO format (YYYY-MM-DD), amounts as numeric values without currency symbols." The AI follows these instructions.

Comparison: Python PDF Extraction Methods

MethodSetup timeOutput formatCustom fieldsScanned PDFsCost
PyPDF25 minRaw text string✗ (you write regex)Free
pdfplumber10 minText + tables✗ (you write parser)Free
AWS Textract1-2 weeksKEY_VALUE_SET blocksPartial (post-processing)$1.50/1K pages
DokyumiVaries with schema setup and representative testingStructured data + confidence and validation✓ (schema-defined)Free with 25 credits/mo

When to Use Each Approach

  • PyPDF2 / pdfplumber: One-off scripts, simple text search, documents where you only need raw text for embedding or search. Not suitable for structured extraction.
  • AWS Textract: If you're already deep in AWS, need very high volume (millions of pages), or require built-in audit trails. Prepare for significant integration work.
  • Dokyumi: When you need schema-defined fields from supported PDF, JPEG, PNG, TIFF, or WEBP inputs. Validate representative documents, review both validation arrays, and complete your integration and business checks before production use.

Get Started

Sign up for free at dokyumi.com — 25 credits/month; one credit covers a document up to five pages, no credit card required. Create a schema, review the suggested fields, and test representative documents. The full API reference (including error codes, TypeScript types, and webhook payload docs) is at dokyumi.com/docs.

These articles are selected from the same editorial cluster, not generated from keyword overlap.

Put build and ship api pipelines to work

Use the API and webhook reference

Confirm request fields, response data, validation, confidence, and webhook signing.

Trace an accounts-payable workflow

See how schema, endpoint, confidence review, and ledger delivery fit together.

Build the first endpoint

Create a schema and test the pipeline against a real source document.

Test the extraction on your own documents

25 free credits each month. One credit covers a document up to 5 pages; self-serve documents can be up to 50 pages. No credit card required.