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
| Method | Setup time | Output format | Custom fields | Scanned PDFs | Cost |
|---|---|---|---|---|---|
| PyPDF2 | 5 min | Raw text string | ✗ (you write regex) | ✗ | Free |
| pdfplumber | 10 min | Text + tables | ✗ (you write parser) | ✗ | Free |
| AWS Textract | 1-2 weeks | KEY_VALUE_SET blocks | Partial (post-processing) | ✓ | $1.50/1K pages |
| Dokyumi | Varies with schema setup and representative testing | Structured 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.
Continue this path
These articles are selected from the same editorial cluster, not generated from keyword overlap.
Put build and ship api pipelines to work
Confirm request fields, response data, validation, confidence, and webhook signing.
See how schema, endpoint, confidence review, and ledger delivery fit together.
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.