Reader board
Which Python library reads PDF, Word, Excel and PowerPoint files without breaking? Which OCR engine reads images best, and which speech-to-text engine makes the fewest mistakes? Document libraries run on the same odd real-world files from the test collections of pdf.js, Apache POI and Apache Tika (bug reports, fuzzers, broken exporters); OCR and speech engines on images and audio with known text. Every new release on PyPI is tested again by itself.
Updated 06 November 2026. Lower is better in the ๐ด ๐ก โฑ columns; the table is sorted by them (the any-document table: by files turned into text). Click a library to
see every file it failed on, with the error, and what changed between its versions.
New releases as they are tested: Atom feed.
๐ Best per format
PDFPyMuPDFfails on 6 of 724 readable filesWord (.docx)docx2txtfails on 1 of 103 readable filesExcel (.xlsx)python-calaminefails on 2 of 297 readable filesPowerPoint (.pptx)markitdownfails on 0 of 66 readable filesAny document โ textKreuzbergtext from 374 of 477 filesOCR (image โ text)PaddleOCRfinds 95.1% of the wordsSpeech to text (English)Parakeetword error rate 11.1%Language detectionpy3langid97.0% correct
1,077 files from Mozilla pdf.js test PDFs;
724 of them give text to at least one library.
| Library | ๐ด Fails where another reads | ๐ก Empty where another reads |
โฑ Hangs or crashes | Text from (all files) | Errors (all files) | Median ms/file | Total seconds |
|---|---|---|---|---|---|---|---|
| 1 PyMuPDF 2.1 | 6 | 7 | 0 | 711 | 14 | 2 | 13.2 |
| 2 pypdfium2 5.15.0 | 6 | 55 | 0 | 663 | 24 | 1 | 10.0 |
| 3 pypdf 7.0.0 since 6.21.4: โผ 1 newly failing | 11 | 49 | 0 | 664 | 34 | 4 | 145.8 |
| 4 pdfminer.six 20260107 since 20251230: โฒ 2 fixed | 11 | 64 | 0 | 649 | 31 | 4 | 179.5 |
| 5 pdfplumber 0.11.10 since 0.11.9: โฒ 1 fixed | 11 | 64 | 0 | 649 | 32 | 5 | 227.6 |
| 6 markitdown 0.1.9 since 0.1.8: โผ 1 newly failing | 12 | 64 | 1 | 648 | 31 | 94 | 516.3 |
Word (.docx)
130 files from Apache POI test files;
103 of them give text to at least one library.
| Library | ๐ด Fails where another reads | ๐ก Empty where another reads |
โฑ Hangs or crashes | Text from (all files) | Errors (all files) | Median ms/file | Total seconds |
|---|---|---|---|---|---|---|---|
| 1 docx2txt 0.9 | 1 | 0 | 0 | 102 | 16 | 1 | 2.9 |
| 2 mammoth 1.13.0 | 2 | 7 | 0 | 94 | 17 | 9 | 14.9 |
| 3 python-docx 1.2.0 | 2 | 13 | 0 | 88 | 17 | 3 | 4.3 |
| 4 markitdown 0.1.9 | 3 | 7 | 0 | 93 | 18 | 174 | 97.3 |
Excel (.xlsx)
353 files from Apache POI test files;
297 of them give text to at least one library.
| Library | ๐ด Fails where another reads | ๐ก Empty where another reads |
โฑ Hangs or crashes | Text from (all files) | Errors (all files) | Median ms/file | Total seconds |
|---|---|---|---|---|---|---|---|
| 1 python-calamine 0.9.1 | 2 | 2 | 0 | 293 | 26 | <1 | 2.0 |
| 2 openpyxl 3.1.5 | 5 | 6 | 0 | 286 | 28 | 6 | 19.4 |
| 3 markitdown 0.1.9 | 11 | 0 | 2 | 286 | 32 | 111 | 315.1 |
PowerPoint (.pptx)
95 files from Apache POI test files;
66 of them give text to at least one library.
| Library | ๐ด Fails where another reads | ๐ก Empty where another reads |
โฑ Hangs or crashes | Text from (all files) | Errors (all files) | Median ms/file | Total seconds |
|---|---|---|---|---|---|---|---|
| 1 markitdown 0.1.9 | 0 | 0 | 0 | 66 | 12 | 71 | 12.9 |
| 2 python-pptx 1.0.2 | 0 | 11 | 0 | 55 | 12 | 9 | 2.7 |
Any document โ text
477 files from Apache Tika test documents;
406 of them give text to at least one library.
| Library | ๐ด Fails where another reads | ๐ก Empty where another reads |
โฑ Hangs or crashes | Text from (all files) | Errors (all files) | Format not supported | Median ms/file | Total seconds |
|---|---|---|---|---|---|---|---|---|
| 1 Kreuzberg 4.10.4 | 11 | 19 | 0 | 374 | 30 | 4 | <1 | 8.7 |
| 2 Unstructured 0.27.36 | 8 | 22 | 0 | 370 | 28 | 11 | 30 | 507.4 |
| 3 Docling 2.148.0 | 10 | 4 | 1 | 266 | 23 | 150 | 7 | 580.7 |
| 4 markitdown 0.1.9 | 4 | 17 | 1 | 253 | 18 | 149 | 125 | 388.2 |
By file type: files read with text
| Type | Kreuzberg | Unstructured | Docling | markitdown |
|---|---|---|---|---|
| .docx 71 files | 60/71 7 empty 4 failed | 57/71 10 empty 3 failed 1 not supported | 59/71 6 empty 6 failed | 57/71 10 empty 4 failed |
| .pdf 71 files | 45/71 22 empty 4 failed | 50/71 16 empty 5 failed | 52/71 14 empty 5 failed | 44/71 21 empty 6 failed |
| .doc 49 files | 37/49 5 empty 7 failed | 37/49 9 empty 1 failed 2 not supported | not supported | not supported |
| .rtf 44 files | 41/44 3 empty | 40/44 2 empty 2 failed | not supported | not supported |
| .html 38 files | 34/38 2 empty 2 failed | 34/38 2 empty 2 failed | 36/38 2 empty | 36/38 2 empty |
| .xlsx 35 files | 22/35 9 empty 4 failed | 21/35 10 empty 4 failed | 20/35 8 empty 7 failed | 20/35 9 empty 6 failed |
| .txt 30 files | 30/30 | 30/30 | 28/30 2 failed | 30/30 |
| .xls 30 files | 18/30 8 empty 4 failed | 18/30 7 empty 5 failed | not supported | 20/30 8 empty 2 failed |
| .pptx 29 files | 22/29 6 empty 1 failed | 21/29 7 empty 1 failed | 23/29 5 empty 1 failed | 22/29 6 empty 1 failed |
| .ppt 22 files | 16/22 6 empty | 16/22 4 empty 2 failed | not supported | not supported |
| .odt 17 files | 15/17 2 failed | 15/17 1 empty 1 failed | 15/17 2 failed | not supported |
| .msg 14 files | 14/14 | 14/14 | 13/14 1 failed | 14/14 |
| .eml 9 files | 9/9 | 9/9 | 9/9 | not supported |
| .odp 4 files | not supported | not supported | 2/4 2 empty | not supported |
| .ods 4 files | 3/4 1 empty | not supported | 4/4 | not supported |
| .csv 3 files | 3/3 | 3/3 | 3/3 | 3/3 |
| .epub 3 files | 3/3 | 3/3 | not supported | 3/3 |
| .json 2 files | 0/2 2 failed | 0/2 2 failed | not supported | 2/2 |
| .md 2 files | 2/2 | 2/2 | 2/2 | 2/2 |
OCR (image โ text)
190 files from OCR test set (CORD receipts, DocLayNet pages, app screenshots);
190 of them give text to at least one library.
| Library | ๐ด Fails where another reads | ๐ก Empty where another reads |
โฑ Hangs or crashes | โ
Words found | Median ms/file | Total seconds |
|---|---|---|---|---|---|---|
| 1 PaddleOCR 3.7.0 Note: With paddlepaddle 3.2.2, as its maintainers advise: with 3.3.1 (the default install) every image fails on CPU (PaddleOCR issue #18162). why | 0 | 0 | 0 | 95.1% | 3070 | 1,527.6 |
| 2 docTR 1.1.0 | 0 | 0 | 0 | 95.0% | 2286 | 864.8 |
| 3 RapidOCR 3.10.0 | 0 | 0 | 0 | 92.4% | 372 | 136.6 |
| 4 EasyOCR 1.7.2 | 0 | 0 | 0 | 75.2% | 3736 | 620.5 |
| 5 Tesseract 0.3.13 (tesseract 5.5.0) | 0 | 24 | 0 | 67.5% | 132 | 53.8 |
By kind of image: words found
| Images | PaddleOCR | docTR | RapidOCR | EasyOCR | Tesseract |
|---|---|---|---|---|---|
| documents 50 images | 91.6% | 92.0% | 90.2% | 71.5% | 88.2% |
| receipts 80 images | 94.8% | 95.5% | 89.4% | 67.7% | 37.6% |
| screenshots 60 images | 98.5% | 96.9% | 98.4% | 88.2% | 90.0% |
Speech to text (English)
130 files from Speech test set (LibriSpeech, FLEURS, AMI meetings, Earnings-22 calls);
130 of them give text to at least one library.
| Library | ๐ด Fails where another reads | ๐ก Empty where another reads |
โฑ Hangs or crashes | โ
Word error rate | Median ms/file | Total seconds |
|---|---|---|---|---|---|---|
| 1 Parakeet 0.12.0 ยท model TDT 0.6B v2 Note: With huggingface-hub 1.31.0: with 1.32+ (new shared cache layout) and current onnxruntime, the model fails to load ("External data path escapes model directory"), as many other ONNX models do. why | 0 | 0 | 0 | 11.1% | 770 | 124.2 |
| 2 faster-whisper 1.2.1 ยท model base.en int8 Note: With PyAV 18.1.0: faster-whisper 1.2.1 (the latest release) fails on every file with PyAV 19 (fixed on its main branch, not released yet). why | 0 | 0 | 0 | 16.8% | 466 | 79.8 |
| 3 whisper.cpp 1.5.1 ยท model base.en | 0 | 0 | 0 | 16.8% | 1226 | 162.9 |
| 4 Whisper 20250625 ยท model base.en | 0 | 0 | 0 | 17.6% | 1142 | 164.9 |
| 5 Moonshine 20251121 ยท model base | 0 | 0 | 0 | 19.8% | 1228 | 167.9 |
| 6 Vosk 0.3.45 ยท model small en-us | 0 | 1 | 0 | 37.1% | 918 | 138.2 |
By kind of clip: word error rate
| Clips | Parakeet | faster-whisper | whisper.cpp | Whisper | Moonshine | Vosk |
|---|---|---|---|---|---|---|
| ami-close-mic 20 clips | 12.4% | 22.7% | 23.4% | 22.7% | 18.3% | 44.3% |
| ami-far-field 15 clips | 29.3% | 47.6% | 46.6% | 49.0% | 50.5% | 79.3% |
| earnings-calls 25 clips | 19.1% | 24.5% | 23.9% | 26.6% | 37.5% | 60.4% |
| fleurs 20 clips | 9.3% | 12.4% | 12.9% | 12.9% | 14.8% | 32.5% |
| librispeech-clean 30 clips | 1.3% | 3.6% | 3.3% | 3.6% | 3.6% | 10.3% |
| librispeech-other 20 clips | 6.4% | 10.9% | 11.8% | 11.5% | 10.6% | 23.8% |
Language detection
760 files from Language test set (Tatoeba sentences, 38 languages);
760 of them give text to at least one library.
| Library | ๐ด Fails where another reads | ๐ก Empty where another reads |
โฑ Hangs or crashes | โ Correct | Median ms/file | Total seconds |
|---|---|---|---|---|---|---|
| 1 py3langid 0.4.0 | 0 | 0 | 0 | 97.0% | <1 | 0.5 |
| 2 Lingua 2.2.0 | 0 | 0 | 0 | 94.6% | <1 | 0.5 |
| 3 fast-langdetect 1.1.0 ยท model fastText lid.176 (lite) | 0 | 0 | 0 | 93.4% | <1 | 1.5 |
| 4 langdetect 1.0.9 | 0 | 0 | 0 | 93.0% | 1 | 1.3 |
| 5 langid 1.1.6 | 0 | 0 | 0 | 89.1% | 1 | 2.5 |
By kind of text: share correct
| Texts | py3langid | Lingua | fast-langdetect | langdetect | langid |
|---|---|---|---|---|---|
| paragraphs 190 texts | 99.0% | 97.4% | 95.8% | 99.5% | 99.5% |
| short-sentences 570 texts | 96.3% | 93.7% | 92.6% | 90.9% | 85.6% |
โ๏ธ Head to head
Any document โ textmarkitdown vs DoclingAny document โ textDocling vs UnstructuredAny document โ textKreuzberg vs DoclingAny document โ textmarkitdown vs UnstructuredAny document โ textKreuzberg vs markitdownAny document โ textKreuzberg vs UnstructuredPDFPyMuPDF vs pdfplumberPDFpypdf vs PyMuPDFPDFpdfplumber vs pypdfPDFpdfplumber vs pdfminer.sixPDFpypdf vs pdfminer.sixPDFPyMuPDF vs pypdfium2PDFmarkitdown vs PyMuPDFWord (.docx)python-docx vs docx2txtWord (.docx)mammoth vs python-docxWord (.docx)markitdown vs python-docxExcel (.xlsx)openpyxl vs python-calaminePowerPoint (.pptx)python-pptx vs markitdownOCR (image โ text)EasyOCR vs TesseractOCR (image โ text)PaddleOCR vs TesseractOCR (image โ text)EasyOCR vs PaddleOCROCR (image โ text)RapidOCR vs PaddleOCROCR (image โ text)RapidOCR vs TesseractOCR (image โ text)EasyOCR vs RapidOCROCR (image โ text)docTR vs PaddleOCROCR (image โ text)docTR vs TesseractSpeech to text (English)faster-whisper vs WhisperSpeech to text (English)faster-whisper vs whisper.cppSpeech to text (English)Vosk vs WhisperSpeech to text (English)faster-whisper vs VoskSpeech to text (English)Parakeet vs faster-whisperSpeech to text (English)Moonshine vs faster-whisperSpeech to text (English)whisper.cpp vs WhisperSpeech to text (English)Parakeet vs WhisperLanguage detectionlangdetect vs fast-langdetectLanguage detectionlangdetect vs LinguaLanguage detectionlangdetect vs langidLanguage detectionLingua vs fast-langdetectLanguage detectionpy3langid vs langidLanguage detectionLingua vs py3langid
How this is measured
- Each library runs a short recipe from its own docs (board/targets.py):
open the file, get all text. Installed with the extras the docs recommend for reading
(
pypdf[full]: AES decryption, Brotli), nothing else. One worker process per CPU; per file 60 s / 120 s / 180 s / 300 s at most (the any-document table allows longer: some tools load models), killed above the memory limit. - Fails where another reads: the library raised an error, hung or crashed on a file from which another library in the same table got text. Files that every library fails on (encrypted, truncated, fuzzed) are not counted against anyone.
- Empty where another reads: no error, but no text. This is often the recipe, not the library: for example
python-docx's paragraphs+tables recipe skips headers, text boxes and nested tables. Check the file list. - Any document โ text: a file type the tool's docs don't list, or that the tool itself rejects as unsupported, counts as "not supported", not as a failure (lists in board/libs.json). Returning a file's raw RTF source counts as empty, for every tool. Each tool runs with default settings: no OCR switched on by hand.
- OCR: each image comes with the human-written ground-truth text published with its dataset. Words found is the share of its words that appear anywhere in the engine's output (reading order doesn't matter); an image that fails or comes back empty counts as 0. The ground truth of receipts and document pages covers only their annotated text, so a character error rate would punish engines for reading more than was annotated; it is in the raw results but not ranked. English models, CPU, default settings.
- Speech: each clip comes with its dataset's reference transcript. Both texts go through Whisper's English text normalizer (case, punctuation, "twenty" = "20"); word error rate is all substituted, missing and extra words over all reference words, the usual corpus-level WER. A clip that fails or comes back empty counts all its words as errors. The three Whisper implementations use the same base.en model; each other engine its documented default model. English, CPU, default decoding settings.
- Language detection: each text's language is known from Tatoeba. An answer counts as correct if the library's code matches after mapping variants to ISO 639-1 (zh-cn โ zh, nb โ no, iw โ he). Close languages count as different: Croatian detected as Bosnian or Serbian, or Norwegian as Danish, is wrong. One call per text with default settings and all languages enabled.
- Text quality (order, tables, layout) is not ranked in the other tables; they are about not breaking. The raw results have every output's size and hash.
- Same files for every version: the test collections are pinned to one commit. GitHub-hosted runner, so times are comparable within one table, roughly.