Tools,

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:

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.

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

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.