| name | pdf |
| description | Use for tasks where a PDF file is the primary input or output: extract text or tables, fill AcroForm fields, redact sensitive or authorship-leaking content, generate PDFs from structured records, inspect page-level statistics, or prove that a produced PDF is readable and non-recoverable where redaction is required. |
PDF Toolkit
PDF is a stream of pages, each carrying text spans, images, form fields, and
annotations. Treat it as a binary container, not a document. Every operation
needs the tool that understands the layer you care about — wrong tool is the
single largest source of silent failure.
Pick the tool by operation
| Operation | Library | Why this one |
|---|
| Page ops, metadata, AcroForm fields | pypdf | Lightweight, no native deps |
| Text + table extraction | pdfplumber | Layout-aware, returns rows |
| Redaction, search-as-rectangles, rendering | PyMuPDF (fitz) | Content-stream surgery |
| Authoring from structured records | reportlab (vector) / weasyprint (HTML→PDF) | Output side |
pypdf cannot extract tables. pdfplumber cannot redact. A black rectangle
drawn over text is not redaction — the underlying text remains in the content
stream and is recoverable.
Inspect before you edit
Always inventory the file first. The type drives every later choice:
from pypdf import PdfReader
r = PdfReader("input.pdf")
print(len(r.pages), r.metadata)
print(list((r.get_fields() or {}).keys())[:20])
print((r.pages[0].extract_text() or "")[:500])
- Empty
extract_text() → scanned/OCR. Switch to pdfplumber or run OCR upstream.
- Non-empty
get_fields() → AcroForm. Use field names below, not visual labels.
- Otherwise → digital text PDF;
pdfplumber for layout, pypdf for page ops.
Filling AcroForm fields
Field names come from get_fields(), not from the visible label near the
field. Labels live in the rendered layout; names are internal IDs. The two
often differ. Fill only the fields the task data supports — leave optional or
court-filled fields blank.
from pypdf import PdfReader, PdfWriter
r = PdfReader("form-template.pdf")
w = PdfWriter()
for p in r.pages:
w.add_page(p)
w.update_page_form_field_values(w.pages[0], {
"field_a": "Jane Doe",
"field_b": "John Smith",
"field_c": "100",
})
with open("form-filled.pdf", "wb") as f:
w.write(f)
If field names are opaque (Text1, Check Box 7), generate an inventory of
(field_name, page_index, rect, label_near_rect) triples first and map by
spatial proximity to readable labels.
Redaction (true removal, not overlay)
Use PyMuPDF to mark and apply redaction annotations. The apply_redactions()
call rewrites the content stream and removes the underlying glyphs:
import fitz
doc = fitz.open("paper.pdf")
needles = ["Alice Smith", "University X", "arXiv:2401.12345"]
for page in doc:
for needle in needles:
for rect in page.search_for(needle):
page.add_redact_annot(rect, fill=(0, 0, 0))
page.apply_redactions()
doc.save("redacted.pdf", garbage=4, deflate=True)
For author/affiliation redaction also strip metadata: doc.set_metadata({})
before saving. Verify by reopening the saved file and re-running text
extraction — every needle string must be absent.
Table extraction
import pdfplumber
import pandas as pd
frames = []
with pdfplumber.open("report.pdf") as pdf:
for page_no, page in enumerate(pdf.pages, start=1):
for table in page.extract_tables():
if not table:
continue
df = pd.DataFrame(table[1:], columns=table[0])
df["source_page"] = page_no
frames.append(df)
combined = pd.concat(frames, ignore_index=True) if frames else pd.DataFrame()
Repeated headers across pages need deduplication. Wrapped cells appear as
multiple rows and need merging by anchor column. Footnote rows and empty
trailing rows need stripping. Numeric columns are strings until you cast.
Generating PDFs from records
reportlab for absolute-positioned content (forms, certificates, receipts).
weasyprint when the source is already HTML/CSS. After producing the file,
verify it programmatically — pypdf.PdfReader opens it, extract_text()
returns the expected strings, page count matches. Some renderers emit visible
text that is not selectable; those PDFs fail downstream extraction.
Pitfalls
- Visual overlay treated as redaction — hidden text recoverable on re-extraction.
- Multi-column text serialised in wrong order by naive extractors — use bbox-aware layout.
- AcroForm: filling visible labels rather than
get_fields() names.
- Table headers repeating per page → duplicated header rows in the combined frame.
- Generated PDF that opens in a viewer but fails re-read by a parser.
- Redaction without metadata strip — author still in
Document Information.