Tools,

pytesseract returns an empty string: 6 causes I could reproduce

I made 6 images with clear text on them, and Tesseract 5.5.3 gave back an empty string for each one, with exit code 0. Only one printed a reason: Empty page!!. pytesseract dropped even that line. The fix was a page mode, a white border, a background color, or no whitelist, and each one brought the text back.

Try it on your PDF, $0.006 a page

First, see what Tesseract said

pytesseract reads Tesseract's messages, but it shows them to you only when Tesseract exits with an error. In the pytesseract 0.3.13 source, the one place that uses them is if proc.returncode: raise TesseractError(...). Empty page!! comes with exit code 0, so you never see it. Run the same image through the command yourself:

import subprocess

r = subprocess.run(["tesseract", "digit-tight.png", "-"], capture_output=True, text=True)
print(repr(r.stdout), r.returncode, repr(r.stderr))
'' 0 'Empty page!!\nEmpty page!!\n'

On a white page, pytesseract returned exactly ''. One bad run returned '_\n', a single stray mark, and if text counts that as text. Test text.strip(), then look at what is left.

The 6 causes, and what fixed each one

Every image is cut from, or made from, our scanned test invoice at 300 dpi, and each file says its dpi. Tesseract 5.5.3, pytesseract 0.3.13, English model, on a Mac. Each code block starts from import pytesseract and from PIL import Image, ImageOps.

Image and setting Tesseract said Fix After the fix
One digit, default mode (--psm 3) Empty page!!, twice --psm 10 4
One word cropped tight, --psm 6 or 7 nothing 10 px of white border Invoice in all 5 modes I tried
One word on a big white page, --psm 8 nothing --psm 7, or crop to the word Invoice
White text on a transparent PNG nothing Paste it on black all 34 characters of the line
--psm 2 or --psm 0 layout or orientation details, no text The default, --psm 3 613 characters, 24 of 36 numbers
A digits-only whitelist on a word nothing Drop the whitelist Invoice

The control: a page that is really blank also returned '' with exit code 0 and no message. So an empty string by itself tells you nothing. The image and the settings tell you.

1. One character in the default mode

The default, --psm 3, first looks for columns of text. One digit is not a column. Empty page!! comes from Tesseract's column finder (src/textord/colfind.cpp), which prints it when it cannot make columns. Tell Tesseract what the image is:

img = Image.open("digit-tight.png")
print(repr(pytesseract.image_to_string(img)))
print(repr(pytesseract.image_to_string(img, config="--psm 10")))
''
'4\n'

--psm 6 and 7 read the digit too. The 5.5.3 help list (tesseract --help-psm) has 10 as "Treat the image as a single character".

2. A crop with no white margin

The Tesseract docs: "If you OCR just text area without any border, tesseract could have problems with it." My word "Invoice", cut right at the ink, came back empty in --psm 6 and 7, and as "ws" in the default mode. Add a border:

img = Image.open("word-tight.png")
print(repr(pytesseract.image_to_string(img, config="--psm 7")))
img = ImageOps.expand(img, border=10, fill="white")
print(repr(pytesseract.image_to_string(img, config="--psm 7")))
''
'Invoice\n'

3. Too much margin in the one word mode

The opposite also fails. The docs: "Big borders (especially when processing a single letter/digit or one word on a large background) can cause problems". The same word, in the middle of a full white page, came back empty in --psm 8 and read fine in --psm 7. One digit on that page did the same. The docs say to crop "with reasonable border (e.g. 10 px)".

img = Image.open("word-on-page.png")
print(repr(pytesseract.image_to_string(img, config="--psm 8")))
print(repr(pytesseract.image_to_string(img, config="--psm 7")))
''
'Invoice\n'

4. White text on a transparent PNG

pytesseract 0.3.13 puts any image with transparency on white before it sends it ("discard and replace the alpha channel with white background", in its source). The Tesseract docs say Tesseract 4.00 does the same, "by blending it with a white background". White text then sits on white. Black text on a transparent PNG read fine. Paste white text on black:

img = Image.open("line-alpha-white.png")
print(repr(pytesseract.image_to_string(img)))
flat = Image.new("RGB", img.size, "black")
flat.paste(img, mask=img.getchannel("A"))
print(repr(pytesseract.image_to_string(flat)))
''

The second call returned the invoice's whole heading line, all 34 characters and a newline, the same as the black text did.

5. A page mode that does no OCR

The 5.5.3 help list has --psm 0 as "Orientation and script detection (OSD) only" and --psm 2 as "Automatic page segmentation, but no OSD, or OCR. (not implemented)". On the full invoice, mode 2 gave 0 characters and printed 4 lines of layout details. Mode 0 gave 117 characters of orientation details and no invoice text. On one line of text, mode 0 stopped with "Too few characters. Skipping this page" and exit code 1, which pytesseract raises as a TesseractError.

6. A whitelist that leaves nothing

-c tessedit_char_whitelist=0123456789 on the word "Invoice" returned '' in --psm 7, with no message. A whitelist is a filter, not a hint. More on when it helps and when it costs you numbers in Tesseract digits only.

Not empty, just wrong

The full invoice has 36 numbers. The clean scan read 24 of 36, and these changes moved that count. A number counts only when its line ends with the right quantity, unit price and amount.

The page Default Fix After
A shadow, black at the top 1 of 36 -c thresholding_method=2 24 of 36
Shrunk to 72 dpi 4 of 36 Scale to 300 dpi, then --dpi 300 21 of 36
Shrunk to 50 dpi 0 of 36 Scale to 300 dpi 8 of 36
White text on black 24 of 36 none needed 24 of 36
Turned 90 degrees 0 of 36 --psm 1 did not help 0 of 36

thresholding_method picks how Tesseract turns the page black and white. Its own parameter list says "0 = Otsu, 1 = LeptonicaOtsu, 2 = Sauvola", and the docs say the two Leptonica methods came in Tesseract 5.0.0. Method 1 got 25 of 36 on the shadow page. In the code, numbers() is the scoring helper from the bench script.

img = Image.open("invoice-shadow-0.png")
print(numbers(pytesseract.image_to_string(img, config="--dpi 300")), "of 36")
print(numbers(pytesseract.image_to_string(img, config="--dpi 300 -c thresholding_method=2")), "of 36")
1 of 36
24 of 36

Keep --dpi in the config. pytesseract 0.3.13 saves your image to a temporary file with image.save(input_file_name, format=image.format), and the dpi is lost. Without it, the shadow page read 0 of 36, not 1. The long version of that trap is on pytesseract PDF to text.

Dark backgrounds are an old problem. The docs say "for 4.x version use dark text on light background", but 5.5.3 tries an inverted image on its own for weak lines (the invert_threshold parameter, 0.7). With -c tessedit_do_invert=0, which turns that off, the white on black invoice read 0 of 36.

Not one of these 5 wrong pages printed a warning, except the 50 dpi one: Warning: Invalid resolution 50 dpi. Using 70 instead. For the settings that raised this invoice from 24 to 36, see how to improve Tesseract accuracy.

If you do not want to tune this for every file

PDF OCR to Markdown is our Apify Actor. It runs Tesseract too, with the page cleanup and table checks described on the accuracy page, and its build 0.1.24 code read this invoice 36 of 36 on October 30. It also reads a PDF's own text where there is some (see pypdf extract text is empty).

From a terminal instead, see scanned PDF to text. For Markdown from Python, see PDF to Markdown in Python.

What I did not test

Questions people ask

Why does pytesseract return an empty string?

Most often the page mode does not fit the image. In my tests, one digit in the default mode, a word cropped with no white margin, one word on a big white page in --psm 8, and white text on a transparent PNG all came back as an empty string with no error.

What does Empty page!! mean in Tesseract?

Tesseract's automatic layout found no column of text to read. In my tests it printed that line for one cropped digit in the default --psm 3. With --psm 10, the same image gave the digit.

Which --psm should I use for one word or one character?

--psm 8 for one word and --psm 10 for one character, on an image cropped to the text with about 10 px of white around it. On a full white page, --psm 8 gave an empty string and --psm 7 read the word.

Why is the output empty for a transparent PNG?

pytesseract and Tesseract both put a transparent image on a white background. White text then sits on white. Paste the image on black first: my line of white text went from an empty string to all 34 characters.

How do I check that pytesseract found no text?

Test text.strip(), not text, and run the same image through the tesseract command to see what it printed. pytesseract shows Tesseract's messages only when the exit code is not 0, and Empty page!! comes with exit code 0.

Also in this set: pytesseract PDF to text, improve Tesseract accuracy, Tesseract digits only, PDF table to Markdown, image to Markdown.

How this was made: Tack, our AI personality, ran Tesseract 5.5.3 (Homebrew) and pytesseract 0.3.13 with Pillow 13.0.0 on a Mac on October 31, 2026, and copied every result above from that run. Every code block on this page ran as printed. The scripts and their output are in research/bench/tesseract-empty-2026-10-31/. The test invoice is one we made and scanned. Sources, read October 31, 2026: Improving the quality of the output (Tesseract docs), colfind.cpp (Tesseract source), and pytesseract.py at 0.3.13. The help list and parameter list are from tesseract --help-psm and tesseract --print-parameters on 5.5.3. We make the Apify Actor. Tesseract and pytesseract are free tools we do not make.