Choosing Between pdfplumber and PyMuPDF for Permit Extraction
Choosing between pdfplumber and PyMuPDF for permit extraction is a design decision you make once per document class and live with across a whole pipeline: pick wrong and you either burn a constrained county VM rasterizing pages the slow way, or you ship an AGPL dependency into a system that can’t carry it. This guide sits under Parsing PDF Permit Applications with OCR and Layout Analysis, part of the broader Automated Permit Ingestion and Parsing Workflows reference, and answers the question that page defers: which library reads which permit document.
The two libraries look interchangeable and are not. pdfplumber is a pure-Python layer over pdfminer.six, built for reading the structure of a digital PDF — words and characters with exact bounding boxes, and mature table detection. PyMuPDF (imported as fitz) is a thin binding over the C library MuPDF, built for speed: fast text extraction and, critically, fast page rasterization for the OCR path. For a municipal parsing subsystem the choice is rarely “one or the other” forever — it is “which one for this stage, on this hardware, under this license.” Get it right and native digital filings read cleanly while scanned filings rasterize cheaply enough to run on a 2–4 GB VM. Get it wrong and you either miss half your tables or blow the memory budget, and — the failure mode that outranks all others in government IT — you may pull PyMuPDF’s AGPL license into a codebase whose procurement rules forbid it. This page frames that decision axis by axis, with a comparison matrix, runnable probes, and a hybrid recommendation.
Permalink to this section Step 1: Classify the Document First — It Decides the Axis
The single fact that dominates the choice is whether the document is a native digital PDF or a scan, because it moves the decision onto a different axis. A digital filing has a real text layer, so the question becomes fidelity — who reads words, coordinates, and tables best. A scan has no usable text, so the question becomes rasterization — who turns pages into images for OCR fastest and cheapest. Classify before you decide, using the same per-page character-count test the parent parser uses.
from __future__ import annotations
from pathlib import Path
import pdfplumber
def is_native_text(pdf_path: Path, min_chars: int = 40) -> bool:
"""True if the document carries a usable text layer (digital, not scanned)."""
with pdfplumber.open(pdf_path) as pdf:
# Sample the first few pages; hybrid docs are handled per-page downstream.
sampled = pdf.pages[:3]
total = sum(len((p.extract_text() or "").strip()) for p in sampled)
return total >= min_chars * len(sampled)
If this returns True, weigh Steps 2 and 4 (fidelity and licensing). If False, weigh Step 3 (speed and memory) — the text-fidelity rows in the matrix simply do not apply, because you will be OCR-ing images either way.
Permalink to this section Step 2: Compare Text and Table Fidelity on Digital Filings
For native filings, pdfplumber’s advantage is granularity. It exposes every character and word with an exact bounding box and offers three table-detection strategies, which is why the sibling guide on extracting tabular data with pdfplumber reaches for it to pull schedules off site plans. PyMuPDF extracts text faster and now ships a find_tables() of its own, but its default text output is block- and line-oriented, which is coarser when you need precise field coordinates for spatial layout analysis.
import fitz # PyMuPDF
# pdfplumber: word-level boxes, ideal for zone-based field mapping.
def words_pdfplumber(pdf_path: Path) -> list[dict[str, float | str]]:
with pdfplumber.open(pdf_path) as pdf:
return pdf.pages[0].extract_words() # each: text, x0, x1, top, bottom
# PyMuPDF: fast structured text, coarser granularity but very quick.
def words_pymupdf(pdf_path: Path) -> list[tuple]:
with fitz.open(pdf_path) as doc:
# "words" gives (x0, y0, x1, y1, word, block, line, word_no).
return doc[0].get_text("words")
Rule of thumb: if the stage needs fractional-zone field mapping or table recovery, pdfplumber earns its slower runtime. If the stage just needs the raw text of a page quickly — a keyword scan, a full-text index, a classification heuristic — PyMuPDF is the leaner choice.
Permalink to this section Step 3: Compare Speed and Memory on Constrained Servers
County deployment hardware is small, and this is where PyMuPDF is decisively ahead. Its C core rasterizes pages far faster than any pure-Python path and holds a much smaller resident footprint, which is exactly what the OCR branch needs: PyMuPDF renders the page to a pixmap, and pdfplumber has no native rasterizer at all. Probe both on your own fixtures rather than trusting a generic benchmark — permit PDFs vary wildly in embedded-image weight.
import time
import fitz
def render_all_pages(pdf_path: Path, dpi: int = 300) -> float:
"""Rasterize every page with PyMuPDF; return seconds. Streams page-by-page
so peak memory stays bounded on a 2-4 GB VM instead of loading all pages."""
start = time.perf_counter()
zoom = dpi / 72.0
with fitz.open(pdf_path) as doc:
for page in doc: # one pixmap live at a time; let each be GC'd
pix = page.get_pixmap(matrix=fitz.Matrix(zoom, zoom))
_ = pix.samples # hand off to OCR here in real code
del pix
return time.perf_counter() - start
For the scanned path there is effectively no contest: use PyMuPDF (or poppler via pdf2image) to rasterize, then send the image to Tesseract as the contractor-license OCR guide describes. pdfplumber never enters that branch.
Permalink to this section Step 4: Weigh the License Before You Ship
This is the criterion that quietly overrides the others in government software. pdfplumber is MIT-licensed — permissive, no copyleft obligations. PyMuPDF is dual-licensed under AGPL v3 or a paid commercial license. AGPL’s network-copyleft clause can obligate you to release your own source if the software is offered over a network, which for a municipal SaaS or a shared multi-jurisdiction portal is a live risk, and many procurement policies prohibit AGPL dependencies outright.
# Encode the licensing decision as data your build can assert on, so an AGPL
# dependency cannot slip into a codebase whose procurement rules forbid it.
LIBRARY_LICENSES: dict[str, str] = {
"pdfplumber": "MIT",
"pymupdf": "AGPL-3.0-or-later", # commercial license available for a fee
}
ALLOWED_LICENSES: frozenset[str] = frozenset({"MIT", "BSD-3-Clause", "Apache-2.0"})
def assert_license_policy(lib: str) -> None:
lic = LIBRARY_LICENSES[lib]
if lic not in ALLOWED_LICENSES:
raise RuntimeError(
f"{lib} is {lic}: obtain a commercial license or route this stage "
f"through an isolated service before shipping."
)
If AGPL is disqualifying for your deployment, the practical answer is to isolate PyMuPDF behind a separate process or service used only for rasterization, or license it commercially — do not let the decision be made by accident at pip install time.
Permalink to this section The Recommendation: Use Both, by Stage
The honest conclusion is that these libraries are complementary, not competing. A production permit parser typically uses PyMuPDF to rasterize scanned pages (fast, low-memory, the OCR branch) and pdfplumber to read native text and tables (precise coordinates, mature table detection, permissive license). Route each document to the right tool by its classification, and confine PyMuPDF to the rasterization stage so its license footprint stays contained.
| Criterion | pdfplumber | PyMuPDF (fitz) | Pick for permits |
|---|---|---|---|
| Native text + layout coords | Char/word bounding boxes | Fast text, coarser blocks | pdfplumber for zone mapping |
| Table detection | lines / text / explicit, mature |
find_tables, newer |
pdfplumber for schedules |
| Raster / render speed | Pure-Python, slower | C-backed MuPDF, fast | PyMuPDF |
| Memory on 2–4 GB VMs | Heavier per page | Low footprint | PyMuPDF |
| Rasterize scans for OCR | No native rasterizer | get_pixmap at any DPI |
PyMuPDF |
| Licensing | MIT (permissive) | AGPL v3 / commercial | pdfplumber unless licensed |
Permalink to this section Parameter and Flag Reference
| Setting | Library | Typical value | Notes for permit workloads |
|---|---|---|---|
min_chars |
pdfplumber | 40 |
Native-vs-scanned threshold; defeats the thin OCR layer copiers add. |
table_settings |
pdfplumber | {"vertical_strategy": "lines"} |
Strategy dict for schedule extraction; see the sibling table guide. |
fitz.Matrix(zoom, zoom) |
PyMuPDF | zoom = dpi/72 |
Render scale; 300 DPI keeps stamps/dot-matrix legible for OCR. |
colorspace |
PyMuPDF | fitz.csGRAY |
Grayscale halves pixmap memory before binarization. |
get_text(...) mode |
PyMuPDF | "words" / "text" |
"words" for boxes, "text" for a fast raw dump. |
pip pin |
both | pdfplumber==0.11.*, pymupdf==1.24.* |
Pin majors; a PyMuPDF bump can shift text-extraction output. |
| license gate | both | ALLOWED_LICENSES |
Assert in CI so AGPL can’t enter without review. |
Permalink to this section Common Failure Patterns and Fixes
Permalink to this section Choosing pdfplumber for the scanned path
pdfplumber cannot rasterize, so using it on scans means an extra dependency and slow, memory-heavy rendering through pdf2image. Classify first (Step 1) and send scanned documents straight to PyMuPDF’s get_pixmap.
Permalink to this section AGPL slipping in unnoticed via a transitive install
pip install never warns about copyleft. Add a license-policy assertion (Step 4) to CI so a PyMuPDF dependency fails the build unless it has been explicitly reviewed and either isolated or commercially licensed.
Permalink to this section Assuming PyMuPDF tables match pdfplumber’s
PyMuPDF’s find_tables() is newer and less battle-tested on the ruled-but-messy schedules of CAD-exported site plans. If a table extraction regresses after switching libraries, move just the table stage back to pdfplumber rather than tuning around it.
Permalink to this section Loading whole documents into memory
Both libraries let you load an entire PDF at once, which spikes memory on small VMs. Stream page-by-page (Step 3) and free each pixmap immediately, regardless of which library you chose.
Permalink to this section Version bumps silently changing text output
A PyMuPDF minor release can alter block/line grouping, shifting downstream regex or zone offsets. Pin majors, snapshot-test extraction against golden fixtures, and treat any diff as a regression to review before deploy.
Permalink to this section Audit and Logging Guidance
Record which library and version handled each document and stage — pdfplumber==0.11.4 text, pymupdf==1.24.9 raster — because a data-quality regression after an upgrade is impossible to diagnose without it, and because a license audit will ask exactly which components touched permit data. Keep the license-policy decision in code and log its outcome per build so a procurement reviewer can see that AGPL was gated, not merely absent by luck. When you split work across both libraries, log the classification that routed each document so a misrouted scan (sent to the text path) or a misrouted digital filing (needlessly rasterized) surfaces as a metric rather than a silent inefficiency. Retain these processing-provenance records alongside the extracted permit data for the state-mandated period, and route unreadable or corrupt inputs into quarantine through the shared error handling and retry logic for ingestion pipelines rather than letting a library exception abort a batch.
Permalink to this section Frequently Asked Questions
Permalink to this section Can I just standardize on one library for everything?
You can, but you will pay for it. Standardizing on pdfplumber means slow, memory-hungry rasterization for scans and an extra rendering dependency; standardizing on PyMuPDF means weaker table detection and an AGPL license across your whole codebase. The lower-cost answer for a mixed municipal document set is to use PyMuPDF for rasterization and pdfplumber for native text and tables, routed by document classification.
Permalink to this section Is PyMuPDF’s AGPL license really a problem for a city?
It can be decisive. AGPL’s network clause may obligate you to publish your own source when the software is served over a network, and many public-sector procurement policies forbid AGPL dependencies without legal sign-off. If that applies, either buy the commercial PyMuPDF license or isolate it behind a rasterization-only service so its license footprint does not spread into your application code.
Permalink to this section Which is faster for pulling plain text out of a digital permit?
PyMuPDF, by a wide margin, because its C core avoids the pure-Python overhead of pdfminer.six that pdfplumber sits on. If all you need is the raw text of a page — for a keyword scan, a classifier, or a full-text index — reach for PyMuPDF’s get_text("text"). Only prefer pdfplumber when you need precise word or character coordinates or its table strategies.
Permalink to this section Related
- Extracting Tabular Data from Permit Site Plans with pdfplumber — the sibling deep dive on why pdfplumber wins the table-extraction stage.
- Extracting Contractor License Numbers from Scanned PDFs with Tesseract — the OCR branch PyMuPDF rasterizes for.
- Parsing PDF Permit Applications with OCR and Layout Analysis — the parent guide whose classify-and-route model this decision plugs into.
- Error Handling and Retry Logic for Ingestion Pipelines — how to quarantine unreadable documents regardless of the library you pick.