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).
- $0.006 a page, plus $0.00005 a run at the default 1 GB of memory. Failed and blank pages 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.
From a terminal instead, see scanned PDF to text. For Markdown from Python, see PDF to Markdown in Python.
What I did not test
- Windows and Linux, other Tesseract versions, and languages other than English. Tesseract 4 can act differently on dark backgrounds.
-
The legacy engine (
--oem 0), and--psm 11, 12 and 13. - Photos from a phone. Every image here comes from one scanned page.
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.