pdfmux/blog
python

Extract text from a PDF in Python: pypdf, PyMuPDF, pdfplumber, and OCR

TL;DRHow to extract text from a PDF in Python: pypdf for simple cases, PyMuPDF for speed, pdfplumber for layout, pytesseract for scanned pages, and when none of them are enough.

Direct answer: For a digital PDF (one with a real text layer), PyMuPDF (import fitz) is the fastest and most complete choice — page.get_text() returns clean text in a few lines. pypdf is a reasonable pure-Python fallback when you can’t install a compiled dependency. pdfplumber is worth the extra weight when you need word positions to fix column order or pull tables. None of the three read scanned pages — for those you need OCR (pytesseract or a cloud OCR API), and you have to detect which pages need it first. pdfmux wraps this whole decision — try the fast path, check the result, fall back to OCR or an LLM only on the pages that actually need it — and scores 0.903 on opendataloader-bench, #2 of 8 engines tested.


The three libraries, and when to reach for each

LibraryInstallSpeedGets youMisses
pypdfPure Python, pip install pypdfSlowest of the threeText, form fields, metadata, merge/splitNo layout info, no OCR, no image extraction
PyMuPDF (fitz)C extension via pip, pip install pymupdfFastest — 10–50x faster than pure-Python extraction on text, ~100x on renderingText, images, rendering, basic table detection, built-in OCR integration (Tesseract/RapidOCR)AGPL license for the open-source build (commercial license available from Artifex)
pdfplumberPure Python, built on pdfminer.sixSlower than PyMuPDFWord-level bounding boxes, table extraction, visual debuggingSlower on large batches; no OCR

If you’re extracting from more than a few hundred PDFs, start with PyMuPDF and only reach for pdfplumber when you specifically need word coordinates. If you’re in a locked-down environment where compiled wheels are a problem, pypdf is the safe default. A fuller breakdown including scanned-document handling and OCR engines is in the best PDF extraction library comparison.


Extracting text with pypdf

from pypdf import PdfReader

reader = PdfReader("invoice.pdf")
text = ""
for page in reader.pages:
    text += page.extract_text() or ""

print(text)

extract_text() can return None on a page with no text layer — the or "" guard above keeps the loop from crashing on a mixed digital/scanned document. pypdf also exposes form fields directly:

fields = reader.get_fields()
if fields:
    for name, field in fields.items():
        print(name, "=", field.get("/V"))

Extracting text with PyMuPDF (fitz)

import fitz  # PyMuPDF

doc = fitz.open("invoice.pdf")
text = ""
for page in doc:
    text += page.get_text()

print(text)

That’s the whole extraction. PyMuPDF’s get_text() takes a mode argument worth knowing about:

page.get_text("text")   # plain text, reading order from the content stream
page.get_text("words")  # list of (x0, y0, x1, y1, word, block_no, line_no, word_no)
page.get_text("dict")   # full structure: blocks → lines → spans, with font + position

"words" and "dict" are what you want when draw order doesn’t match reading order (see the FAQ below, or the dedicated multi-column PDF extraction post). PyMuPDF also gives you a one-line way to check whether a page is actually scanned before you waste time on OCR:

def is_scanned(page):
    text = page.get_text().strip()
    images = page.get_images()
    return len(text) < 10 and len(images) > 0

Extracting text with pdfplumber (when you need layout)

import pdfplumber

with pdfplumber.open("invoice.pdf") as pdf:
    for page in pdf.pages:
        text = page.extract_text()
        words = page.extract_words()  # each word with x0, y0, x1, y1

extract_words() is the reason to pick pdfplumber over the other two: every word comes back with its bounding box, so you can re-sort text by column before concatenating it, or feed the coordinates into a table-extraction pass. If tables are the actual goal rather than a side effect, go straight to extracting tables from PDFs in Python — it covers pdfplumber.extract_table() and when a dedicated table model beats it.


When you get garbage back instead of text

Three failure modes show up constantly, and they look similar but need different fixes.

Empty string, page has images. The page is a scanned document — a photographed or print-to-PDF page with no underlying text layer. extract_text() isn’t broken; there’s nothing for it to extract. Detect it with the is_scanned() check above, then route to OCR. The full detection logic (including partially-scanned documents, which are the common real-world case) is in detecting scanned PDFs in Python.

Garbled characters or wrong ligatures (fi rendering as a box, or random accented characters where plain ASCII should be). This is usually a broken or non-standard font CMap in the PDF — the visual glyph is correct but the underlying character code the extractor reads is wrong. Fixing garbled PDF text in Python walks through the CMap repair and the fallback-to-OCR escape hatch when the font is unrecoverable.

Right characters, wrong order. Covered in the FAQ above — this is a layout problem, not an extraction problem, and the fix is re-sorting by position, not switching libraries.

OCR for scanned pages

Once you’ve flagged a page as scanned, render it to an image and run OCR:

import fitz
import pytesseract
from PIL import Image
import io

doc = fitz.open("scan.pdf")
page = doc[0]
pix = page.get_pixmap(dpi=300)  # 300 DPI is the usual floor for readable OCR
img = Image.open(io.BytesIO(pix.tobytes("png")))
text = pytesseract.image_to_string(img)

DPI matters more than most of the OCR tuning you’ll find elsewhere — under ~200 DPI, Tesseract’s character error rate climbs fast regardless of any other setting. If you’re OCR’ing a whole batch and want to also produce a searchable PDF (not just raw text) for archival, see converting a scanned PDF to a searchable PDF in Python.


When this whole decision tree gets old: pdfmux

Every example above assumes you already know which page needs which path. In a real batch — invoices from different scanners, contracts with some signed pages photographed and some born-digital, financial reports with dense tables — you don’t know that ahead of time, and the usual failure mode is a silent partial extraction: get_text() returns something, just not all of it, and nothing tells you that happened.

import pdfmux

result = pdfmux.convert("mixed-batch.pdf")
print(result.markdown)
print(result.confidence)  # per-page confidence score

pdfmux runs PyMuPDF first, audits the output quality per page, and only falls back to OCR or an LLM on the pages that fail the audit — attaching a confidence score to each page so a low-confidence result is visible instead of silent. It scores 0.903 overall on opendataloader-bench (reading order 0.920, table structure 0.911), #2 of 8 engines tested, just behind opendataloader-hybrid at 0.909 — both open-source. pip install pdfmux, MIT licensed.

For a side-by-side against the library this post leads with, see pdfmux vs PyMuPDF.

Frequently asked questions

Why does my extracted text have the words in the wrong order?

The library is reading text runs in the order they were drawn into the PDF, not the order a person reads them. Multi-column layouts, floating sidebars, and footnotes are the usual cause. pdfplumber and PyMuPDF both expose word-level bounding boxes so you can re-sort by position (left-to-right, top-to-bottom within a column) instead of trusting draw order.

pypdf returns an empty string — is the PDF broken?

Almost always no. An empty string from pypdf or PyMuPDF means the page has no extractable text layer — it's a scanned image, a flattened print-to-PDF, or a photographed document. Check with `page.get_text()` returning `''` plus a non-zero `page.get_images()` count, then route to OCR.

Should I use pypdf or PyMuPDF (fitz)?

pypdf is pure Python, easier to install in restricted environments, and fine for short documents and form-field work. PyMuPDF is a C library with Python bindings and is meaningfully faster on anything beyond a handful of pages, plus it gives you image extraction and rendering in the same package. For a straight text-extraction job on more than a few hundred pages, PyMuPDF is the default choice.

Does pdfmux replace all of this?

It replaces the decision tree, not the libraries — pdfmux runs PyMuPDF first, audits the result, and routes failing pages to OCR or an LLM fallback automatically, attaching a confidence score to every page so you know which output to trust without eyeballing it.