DEV Community

Cover image for I Tried 3 Python PDF Parsers. The Results Were Surprisingly Different
Simon Briggs
Simon Briggs

Posted on

I Tried 3 Python PDF Parsers. The Results Were Surprisingly Different

I assumed extract_text() was just extract_text(). Pick any library; get the same string back.

Then I fed one PDF to three parsers and got three different strings. One put my page footer on line two. One wove two columns into a single stream of nonsense. One (all of them, actually) returned nothing at all for a scanned copy.

Here's what I ran and what I'd pick for which job.

The setup

I built a one-page test PDF with reportlab. It's small, but it packs in the things that usually break parsers:

  • a header ("ACME Corp | Q3 Report | CONFIDENTIAL")
  • a two-column body
  • a ruled table (5 rows, 4 columns)
  • a "Page 1" footer

I also rendered that page to an image and wrapped it in a new PDF, so I'd have a "scanned" version with no text layer.

Versions: pypdf 5.9.0, pdfplumber 0.11.9, PyMuPDF 1.28.2. Each ran 50 times on the file and was averaged.

import pypdf, pdfplumber, pymupdf

f = "test.pdf"
a = pypdf.PdfReader(f).pages[0].extract_text()
b = pdfplumber.open(f).pages[0].extract_text()
c = pymupdf.open(f)[0].get_text()
Enter fullscreen mode Exit fullscreen mode

Three lines. Three outputs.

pypdf: fast enough, order is a gamble

pypdf averaged 2.9 ms per run. It's pure Python with no dependencies, which is why it's still my default for merging, splitting, and reading metadata.

The text came out in the order the PDF writes it to the page, not the order your eyes read it. So the header was followed immediately by "Page 1" (the footer), then the entire left column, then the entire right column.

That's actually fine for the columns. Each one stayed intact. The footer placement is the annoying part if you're feeding this to a search index or an LLM.

The table came back with every cell on its own line:

Item
Qty
Unit Price
Total
Widget A
10
...
Enter fullscreen mode Exit fullscreen mode

You can rebuild rows by grouping in fours, but only if you already know the table has four columns.

pdfplumber: the layout-aware one (and it shows)

pdfplumber averaged 32.8 ms. That's roughly ten times slower than the other two on this page.

What you get for the wait is text arranged by position. The footer landed at the bottom where it belongs, and the table rows stayed on single lines:

Widget A 10 $4.50 $45.00
Widget B 3 $12.00 $36.00
Enter fullscreen mode Exit fullscreen mode

The catch showed up in the body. It merged the two columns line by line, so one line read "Left column text about revenue growth in the Right column text about customer churn and". If you don't check for columns, you'll ship that into production without noticing.

The fix is to crop first:

page = pdfplumber.open(f).pages[0]
left = pagecrop((0, 0, page.width / 2, page.height))
right = page.crop((page.width / 2, 0, page.width, page.height))
print(left.extract_text())
print(right.extract_text())
Enter fullscreen mode Exit fullscreen mode

Its real strength is extract_tables(). On my ruled table, it returned a clean list of lists, header row included, empty cells as empty strings. No fiddling.

PyMuPDF: the speed pick with a licensing footnote

PyMuPDF was the fastest at 2.0 ms. Its default get_text() output matched pypdf's ordering, footer-after-header quirk included.

Two things stood out.

First, find_tables() returned the same clean table as pdfplumber, at a fraction of the runtime. If you're processing thousands of PDFs, that gap matters.

Second, get_text(sort=True) re-sorts blocks by position. On my two-column page, it padded the text with spaces to place the columns side by side, which reads more like the visual page but isn't what you want for clean prose. Sorting helps, but it isn't magic.

One non-technical note. PyMuPDF is AGPL licensed, with a commercial license available from Artifex. For a personal script, ignore this. If you're shipping it inside a closed-source product, read the license before you build on it.

The scan test: a three-way tie at zero

All three returned an empty string for the image-only version. Not garbled text. Nothing.

That's the behavior to build around. These libraries read the text layer that's already inside the file, and a scan has none. They don't do OCR.

A cheap guard in your pipeline:

text = page.extract_text() or ""
if len(text.strip()) < 20:
    # probably a scan, send to OCR (pytesseract, ocrmypdf, etc.)
    ...
Enter fullscreen mode Exit fullscreen mode

Without something like this, an empty result looks like "the PDF has no content" instead of "the PDF has no text layer." Those need very different handling.

What I'd actually pick

Job My pick
Merge, split, metadata pypdf
Tables, or mixed layouts pdfplumber (slow but forgiving)
Big batches, speed matters PyMuPDF
Scans none of them; use OCR first

If I could only install one, I'd go with pdfplumber for anything table-heavy and PyMuPDF for volume.

Caveats, so you don't over-trust my results

My test file was generated by reportlab, which writes content in a fairly logical order. A PDF exported from InDesign, a browser print dialog, or an old accounting system can order its text stream very differently. That would change how pypdf and PyMuPDF behave.

I also only tested a ruled table. Borderless tables (the kind with just whitespace between columns) are much harder for every library here, and you'll likely need to tune settings.

And timings depend on your machine. Trust the ratios more than the exact milliseconds.

A habit that saves me debugging time

Before I write parser code for a new kind of PDF, I look at it as a human first. Is that "table" a real table or a stack of text boxes? Is the text selectable or a scan? Does it have hidden columns?

Sometimes the quickest way to find out is to push the file through a converter and see what structure survives. I'll run a suspicious PDF through PDF Conveter to PDF to Excel. If the output comes back as a real grid, I know the layout is table-like. If it comes back as floating text boxes, I know to expect trouble.

It's also handy when a non-dev teammate just needs the numbers in a spreadsheet, and I'd rather not write a script for a one-off.

If you want to check how a file behaves before you commit to a parser, PDF Conveter is free to try, no script required.

Try it on your own files

Run the three-line snippet above on the ugliest PDF you have. Multi-column reports and old invoices are good candidates. The differences will show up fast.

If you find a case where one of these libraries behaves differently from what I saw, drop it in the comments. I'm curious which PDFs break which parser.

Top comments (0)