pypdf extract text is empty: the page is a scan, not a bug
pypdf reads the text layer a PDF already holds. It does not look at pictures, and a scanned page is one big picture. On my test invoice the digital PDF gave 563 characters and all 36 numbers in 0.01 seconds. The same page, saved as a scan, gave an empty string and 0 of 36. No setting in pypdf will find that text. OCR will.
Try it on your PDF, $0.006 a page
The empty string
from pypdf import PdfReader
page = PdfReader("invoice-scan.pdf").pages[0]
print(repr(page.extract_text()))
''
That is the whole output: 0 characters, 0 words, 0 of 36 numbers, in 0.00 seconds. No warning, no error. pypdf did its job. The page has nothing to read.
pypdf's docs say it in their own words: "pypdf is not OCR software", and "pypdf will also never be able to extract text from images". They also cover your exact case: "If a PDF page appears to contain only an image (e.g., a scanned document), the extracted text may be minimal or visually empty. In such cases, consider using OCR software such as Tesseract OCR to extract text from images."
Two lines tell you which PDF you have
page = PdfReader(path).pages[0]
print(len(page.images), repr(page.extract_text()[:40]))
On my three test files, those two lines printed:
| File | len(page.images) |
Text |
|---|---|---|
| Digital invoice, made in a program | 0 | 563 characters, starts with the invoice heading |
| Same page, saved as a scan | 1 | empty string |
| Scan with a scanner's text layer | 1 | 623 characters, heading and all |
Use page.extract_text().strip() when you test for empty,
not if not text. A page can return spaces or a stray
character, and those pass an emptiness test while telling you nothing.
The three kinds of PDF
pypdf's docs sort PDFs into digitally born files, scanned files, and OCRed files, where a scanner put its own text in the background. My three test files are one of each, and pypdf scored them like this. A number counts only when its line ends with the right quantity, unit price and amount.
| Kind | pypdf read | Numbers right | Time |
|---|---|---|---|
| Digital | 563 characters | 36 of 36 | 0.01 s |
| Scan | empty string | 0 of 36 | 0.00 s |
| Scan with a scanner's text layer | 623 characters | 23 of 36 | 0.05 s |
The third row is the one people miss. The file looks fine, the text comes out, and 13 of 36 numbers are wrong because the scanner's mistakes were saved into the PDF. The docs warn about exactly this: "Although the scanning software (OCR) is pretty good today, it still fails once in a while. pypdf is no OCR software; it will not be able to detect those failures."
If it is digital, ask for the layout
A digital page needs no OCR. OCR on it can only add mistakes: the docs say pypdf "will never confuse characters. It just reads what is in the file." What pypdf does badly by default is columns. The plain read gives you one line of words. Layout mode keeps the spacing:
text = page.extract_text(extraction_mode="layout")
The docs describe it as "extract text in a fixed width format that
closely adheres to the rendered layout in the source pdf". On my
digital invoice it returned 883 characters against 563 for the plain
read, and the same 36 of 36 numbers. The extra 320 characters are
spacing. If you want the blank lines gone, add
layout_mode_space_vertically=False.
If it is a scan, OCR it
Render the page to an image at 300 dpi, then hand the image to Tesseract. That is the short version. The long version, with the code for three libraries and the dpi trap that took a scanned invoice from 24 numbers right to 0, is on pytesseract PDF to text. If you want the command line instead of Python, see scanned PDF to text, and for the settings that move the score, see how to improve Tesseract accuracy.
One check before you OCR: 36 of 36 numbers on the digital page is not a result Tesseract matches. On this invoice, the best OCR run in my tests got 25 of 36, and our Actor got 36 of 36 on October 30 because it reads the text layer where there is one and only OCRs the pages that need it (the run is on Tesseract digits only).
For many files, or from your own code, PDF OCR to Markdown is our Apify Actor:
- $0.006 a page, plus $0.00005 a run at the default 1 GB of memory. Pages that fail are free.
- The Apify free plan lets you try 20 pages a run and 50 a month.
- Pages are read inside the run. They are not sent to any AI service.
Want Markdown instead of text? See PDF to Markdown in Python, where I timed 5 libraries on the same invoice, and PDF table to Markdown for tables.
What I did not test
- PDFs with more than one page, password protected PDFs, and pages where the text sits inside a form. This is one page we made.
- Windows, and pypdf's strict mode.
- Memory. The docs warn that a page's content stream can be large: "we have seen 10 GB RAM being required for an uncompressed content stream of about 300 MB". I did not measure it.
- Speed you can trust. One page, on a Mac that was busy.
Questions people ask
Why does pypdf return an empty string?
The page is a picture of a page, and pypdf reads only the text layer a PDF already carries. On our test invoice the digital page gave 563 characters and the same page saved as a scan gave an empty string.
Is pypdf broken?
No. pypdf's own docs say it is not OCR software and will never be able to extract text from images. It read all 36 numbers on the digital page of our invoice.
How do I know if a PDF is scanned?
Two lines: print len(page.images) and the first
characters of page.extract_text(). On our scan those
printed 1 image and an empty string. On our digital page, 0 images and
the invoice heading.
Can pypdf read a scanned PDF at all?
Only the text a scanner already added. Our scan with a scanner's text layer gave 623 characters, but just 23 of 36 numbers were right, because the OCR mistakes were saved into the file. pypdf's docs suggest running OCR software yourself instead.
What should I use instead of pypdf for a scan?
OCR software. Render each page at 300 dpi and run Tesseract on it, free and local. For many files, our Apify Actor checks each page for a text layer, OCRs only the pages that need it, and read 36 of 36 numbers on this invoice at $0.006 a page.
How this was made: Tack, our AI personality, ran pypdf 6.21.4 on Python 3.13.11 on a Mac on October 31, 2026, and copied every count above from that run. The script and its output are in research/bench/pypdf-text-2026-10-31/. The digital invoice is one we made. The scan is the same page saved as a 300 dpi JPEG with no text layer, and the third file is that scan after pytesseract wrote a text layer into it on October 30. Facts about pypdf come from its own docs, read October 31, 2026: Extract Text from a PDF. We make the Apify Actor. pypdf and Tesseract are free tools we do not make.