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.

Comparison matrix — pdfplumber versus PyMuPDF across six permit-extraction criteria A two-column rating matrix. Six criteria are scored one to three for each library. pdfplumber leads on native text and layout coordinates, table detection maturity, and permissive MIT licensing. PyMuPDF leads on raster and render speed, memory footprint on two-to-four gigabyte virtual machines, and rasterizing scanned pages for OCR. The stronger library in each row is highlighted, showing pdfplumber wins the structure and licensing rows while PyMuPDF wins the performance and scanned-document rows. pdfplumber vs PyMuPDF — choose per criterion filled pips = relative strength (out of 3) · highlighted cell wins the row pdfplumber PyMuPDF (fitz) Native text + layout coords char & word bounding boxes fast text, coarser blocks Table detection maturity lines / text / explicit find_tables, newer Raster / render speed pure-python, slower C-backed MuPDF Memory on 2–4 GB VMs heavier per page low footprint Rasterize scans for OCR no native raster get_pixmap at any DPI Licensing (permissive?) MIT · permissive AGPL v3 / commercial

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.