Tesseract with multiple languages: put the main one first, tested on 19 pages
Join the codes with a plus sign, as in lang="fra+eng",
and put the page's main language first. On 4 test pages with French,
German and Spanish text, English first read 46 of 56 accented words
right, and the main language first read 54. If both languages use the
Latin alphabet, you may not need English at all:
fra alone read all 117 English words on a page in English
and French. And a missing second model is only a warning, so check
that it is there.
Try it on your PDF, $0.006 a page
The code
Tesseract 5.5.3, pytesseract 0.3.13, Python 3.14.7, on a Mac, with the tessdata_fast 4.1.0 models. The check stops you when a model is missing, because Tesseract itself will not:
import pytesseract
langs = ["fra", "eng"] # the page's main language first
missing = set(langs) - set(pytesseract.get_languages())
if missing:
raise SystemExit(f"No Tesseract model for: {sorted(missing)}")
print(pytesseract.image_to_string("notice.png", lang="+".join(langs)))
On the command line, the same is
tesseract notice.png - -l fra+eng. With the Homebrew
models alone (eng, osd and
snum), the code above stopped with
No Tesseract model for: ['fra']. With the French file in
place, it read my English and French notice with no wrong word. The
end of the output:
Avis à tous les locataires
L'eau sera coupée le jeudi 12 novembre, de 9 h à 13 h, pendant que le plombier remplace la vanne principale au
...
enfants loin de l'escalier, où l'équipe dépose ses outils.
The order changes the result
The Tesseract docs say it in one line: "The time taken for OCR as well as the output can be different based on the order of languages." I measured it. Each test page is one we wrote and made into a scan on a computer: a tenant notice in French, German and Spanish, and an English menu with French dish names. The light set uses Times with a small blur. The rough set uses Arial with more blur, more noise and a lower JPEG quality. The score is how many words with an accent or an umlaut came out exactly right, of 56 on each set of 4 pages.
| Setting (French page shown) | Light set | Rough set |
|---|---|---|
eng |
11 of 56 | not run |
eng+fra, English first |
46 of 56 | 50 of 56 |
fra+eng, main language first |
54 of 56 | 56 of 56 |
fra alone |
55 of 56 | 56 of 56 |
script/Latin |
56 of 56 | 55 of 56 |
The German page and the Spanish page used deu and
spa in the same places. What English first got wrong:
-
German, with
eng:fürbecamefiir,StörungbecameStrungandGrößerebecameGréBere. Witheng+deu,fiirandwahrendstayed. Withdeu+eng, every word was right. -
Spanish, with
eng+spa:cortará,válvulaandAvíseleslost their accents. Withspa+eng, onlyAvíselesdid. -
The menu, with
eng+fra:PâtébecamePatéandbrûléebecamebrilée. Withfra+eng, onlybrûléewas still wrong, asbrilée.
The order held on Linux too. In Docker, on Debian with Tesseract
5.5.0, the French page read 15 of 17 accented words with
eng+fra and 17 of 17 with fra+eng.
A page in English and one more Latin language
Three pages held the English notice and then the same notice in French, German or Spanish. The English half has 117 words, so 351 on the 3 pages, and the other half has 41 accented words.
| Setting | English words | Accented words, light | Accented words, rough |
|---|---|---|---|
eng |
351 of 351 | 6 of 41 | not run |
eng+fra |
351 of 351 | 36 of 41 | 35 of 41 |
fra+eng |
351 of 351 | 38 of 41 | 35 of 41 |
fra alone |
351 of 351 | 41 of 41 | 41 of 41 |
script/Latin |
351 of 351 | 41 of 41 | 41 of 41 |
The French, German and Spanish models read every English word on these
pages, and adding eng cost accents. So for two Latin
languages, try the other language alone first, and compare on a few of
your own pages.
script/Latin is one model for all languages in the Latin
alphabet. It read 97 of 97 accented words on the light set and 96 on
the rough set. The one-language models read 96 and 97, so it was no
better on these pages. And its file is 89,384,811 bytes, against
1,130,365 for fra. It peaked at 344 to 354 MB of memory
per page, against 75 to 111 MB for one or two language models.
Two alphabets: list both
When the second language has its own alphabet, list both.
eng could not read the Russian half, and
rus could not read the English half. Each read the other
alphabet as look-alike letters of its own. On the Russian page,
eng read the heading "Объявление для всех жильцов" as
OObaBJIeHHe 10 BCeX 2KHJIbILOB. On the two-language page,
rus read "The water will be shut off" as
Тье \уаег \7И Бе зВиЕ о оп. Chinese did better:
chi_sim alone still read 114 of 117 English words.
| Setting | English words | The other half |
|---|---|---|
eng |
117 of 117 | Russian words: 6 of 84 |
rus |
6 of 117 | 84 of 84 |
eng+rus or rus+eng |
117 of 117 | 84 of 84 |
script/Cyrillic |
6 of 117 | 84 of 84 |
eng |
117 of 117 | Chinese: no Chinese characters at all |
chi_sim |
114 of 117 | 4 errors in 163 characters and marks, and 1 space |
eng+chi_sim |
117 of 117 | 4 errors, and 127 spaces between characters |
chi_sim+eng |
117 of 117 | 4 errors, and 1 space |
script/HanS |
117 of 117 | 2 errors, and 126 spaces |
The spaces come from the order. With English first, the Chinese
heading 全体住户通知 came out as 全 体 住户 通知. Written
Chinese has no spaces between words, so a plain text search for
全体住户 no longer finds it. With chi_sim+eng, the same
heading came out whole. script/HanS, with no
eng at all, also put the spaces in. "Errors" here is the
edit distance on the 163 Chinese characters and punctuation marks.
Every model costs time and memory
One thread (OMP_THREAD_LIMIT=1), median of 3 runs, peak
memory from /usr/bin/time -l. On the English notice,
where every setting below read every word:
| Setting | Time a page | Peak memory |
|---|---|---|
eng |
0.31 s | 84 MB |
eng+fra |
0.34 s | 98 MB |
eng+fra+deu+spa |
0.41 s | 128 MB |
eng+fra+deu+spa+por+rus+chi_sim
|
0.47 s | 205 MB |
script/Latin |
0.46 s | 351 MB |
The Chinese models cost more. On the Chinese notice,
chi_sim took 0.40 seconds and all 7 took 1.24 seconds.
And more languages did not mean safer. On the Spanish page,
eng+fra+deu+spa read 6 of 12 accented words where
eng+spa read 9, and it turned
calefacción into calefacciön, with a German
letter. On the Chinese page, all 7 made 8 errors in the same 163,
against 5 for chi_sim. List the languages your pages
have, and no more.
Ways the language string goes wrong
All on the German notice, with the CLI and with pytesseract, except
the Debian row (the French page, CLI only). "Fails" means exit code 1
and, in pytesseract, a
TesseractError.
| What you pass | What happened |
|---|---|
"eng+deu", with no German file |
Exit 0. Tesseract printed
Failed loading language 'deu' and read in English
only. pytesseract raised nothing and returned
Hinweis fiir alle Mieter. Same with
"deu+eng".
|
"deu", with no German file |
Fails:
Error opening data file .../deu.traineddata, then
Tesseract couldn't load any languages!
|
"eng,deu" or "eng deu" |
Fails: Tesseract looks for one file named
eng,deu.traineddata or
eng deu.traineddata
|
"eng + deu" |
Fails: the spaces stay in the names, as
'eng ' and ' deu'
|
"en+de", "ger" or
"German"
|
Fails. The code is the file name: eng,
deu
|
["eng", "deu"], a Python list |
TypeError: expected str, bytes or os.PathLike object, not
list
|
"ENG+DEU" |
Worked on my Mac, whose disk ignores case. On Debian,
"ENG+FRA" failed with
Failed loading language 'ENG'
|
"eng+deu+" or "eng++deu" |
Worked, with no warning |
The first row is the one to fear: no error, and quietly worse text.
That is why the code at the top checks
get_languages(). If you keep your models in your own
folder, give the check the same folder, with
TESSDATA_PREFIX or the same
--tessdata-dir config.
Get the language files
The code for each language is its file name in tessdata_fast:
eng, fra, deu,
spa, por, rus and
chi_sim are the ones I used. On Debian or Ubuntu, each
language is a package. Note the dash in the package name, where the
code has an underscore:
apt-get install -y --no-install-recommends tesseract-ocr-fra tesseract-ocr-deu tesseract-ocr-chi-sim
On python:3.13-slim (Debian 13.7), as root in Docker, those 3 packages added 5 MB. Their files had the same SHA-256 as the tessdata_fast 4.1.0 files, so they are the models I tested.
On my Mac, Homebrew's Tesseract came with eng,
osd and snum only. Homebrew has a
tesseract-lang formula for more languages, from
tessdata_fast 4.1.0, which I did not install. I downloaded the files I
needed into a folder of my own:
curl -sSfL -o fra.traineddata "https://raw.githubusercontent.com/tesseract-ocr/tessdata_fast/4.1.0/fra.traineddata"
Then I set TESSDATA_PREFIX to that folder, or passed
--tessdata-dir. tesseract --list-langs shows
what Tesseract can see. For tessdata_best, the larger models, see
tessdata_best vs fast.
Tesseract will not pick the language for you
Its orientation and script check, --psm 0, names an
alphabet, not a language. On my one-language pages it said
Script: Latin for English, French, German and Spanish
alike, Cyrillic for Russian and Han for
Chinese. That is enough to choose between eng+rus and
chi_sim+eng, but not between fra and
deu. The
orientation page
shows the same check getting the script wrong on a sideways page.
If you would rather not manage models
PDF OCR to Markdown is our Apify Actor. It runs Tesseract with tessdata_fast 4.1.0 models, and its files for the 7 languages above are the ones I tested here. It reads 10 languages: English, Spanish, French, German, Portuguese, Russian, Arabic, Hindi, Chinese (simplified) and Japanese. You choose one language per run. Pages that already have a text layer are read as they are, in any language. You paste links to PDFs or images, or call it as an API, and get Markdown back.
- $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.
I did not run these test pages through the Actor. If your own Tesseract works, keep it: it is free and works offline. For a whole PDF, see pytesseract PDF to text.
What I did not test
- Real scans. All 19 pages are ones we wrote and made into scans on a computer, from one notice and one menu. Real pages can score lower, and your best setting can differ.
- Languages beyond these 7, right-to-left text such as Arabic and Hebrew, and Hindi.
-
tessdata_best and the legacy engine (
--oem 0). Every run used tessdata_fast 4.1.0. - Windows, tesseract.js, and Tesseract 5.5.4, which Homebrew now lists as stable. I ran 5.5.3 on the Mac and 5.5.0 on Debian.
- Handwriting, or two languages in one line of a table.
Questions people ask
How do I use two languages in Tesseract?
Join the language codes with a plus sign: tesseract page.png - -l fra+eng on the command line, or lang="fra+eng" in pytesseract. Each code needs its .traineddata file in Tesseract's data folder.
Does the order of languages matter in Tesseract?
Yes. On 4 test pages with French, German and Spanish text, English first read 46 of 56 accented words right, and the main language first read 54. With English first, Chinese came out with a space between the characters.
How do I pass multiple languages to pytesseract?
Pass one string, such as lang="fra+eng". A Python list raised TypeError, and a comma or a space between the codes made Tesseract look for a file with that whole name.
Why does Tesseract ignore my second language?
Its model file is probably missing. With lang="eng+deu" and no German file, Tesseract printed Failed loading language 'deu', read in English and exited 0, so pytesseract raised nothing. Check pytesseract.get_languages() first.
Can Tesseract detect the language by itself?
No. Its script check, --psm 0, said Latin for my English, French, German and Spanish pages alike. It can tell Cyrillic or Han from Latin, but not French from German.
Does adding languages slow Tesseract down?
Yes, a little each time, and it uses more memory. On my English page, eng took 0.31 seconds and 84 MB, and 7 languages took 0.47 seconds and 205 MB. On the Chinese page, 7 languages took 1.24 seconds against 0.40.
Also in this set: improve Tesseract accuracy, tessdata_best vs fast, TesseractNotFoundError, pytesseract empty string, Tesseract psm modes, EasyOCR vs Tesseract, scanned PDF to text.
How this was made: Tack, our AI personality, ran these tests on a Mac and in Docker on November 1, 2026, and copied every result above from that run. The Python block and the curl line ran as printed. The apt line ran as root in Docker, with -qq and the tesseract-ocr package already in. Each language model and osd was checked against the SHA-256 list our Actor pins. The 3 script models are not on that list, and their SHA-256 values are in scripts.txt. The scripts and their output are in research/bench/tesseract-languages-2026-11-01/. Sources, read November 1, 2026: Tesseract command line usage, the tesseract manual at 5.5.0, the pytesseract README at 0.3.13, tessdata_fast, Homebrew's tesseract-lang and Debian's tesseract-ocr-fra. We make the Apify Actor. Tesseract, pytesseract and the language models are free tools we do not make.