OCRRouterGet API key

Python OCR: pytesseract, EasyOCR, PaddleOCR and an API, with code that runs

OCR in Python with four options, each run on the same invoice: pytesseract, EasyOCR, PaddleOCR 3 and an OCR API. Code, real output and which to pick.

Updated

TL;DR

  • pytesseract read every word of our printed invoice, but returned the table as plain lines.
  • EasyOCR read most dollar signs as S and skipped single-digit numbers on the same file.
  • PaddleOCR 3 was accurate but uses a new predict() API; most tutorials online are for version 2.
  • An OCR API returned the line items as a Markdown table, which the local libraries could not do.

Python has four practical ways to do OCR: pytesseract (a wrapper around the Tesseract engine), EasyOCR, PaddleOCR, and calling a hosted OCR API over HTTP. This guide gives you code for each one that we ran on the same invoice image on November 15, 2026, shows what each returned, and tells you which to pick.

All four scripts read the same file: the 1000 x 760 px sample invoice from our homepage. You can download it here and follow along.

Which Python OCR library should you use?

Short answer, from our run on a clean, printed invoice:

OptionInstall sizeWords wrong on our invoiceKeeps table structureRuns offline
pytesseract + Tesseract 5.5.4Small (needs the Tesseract binary)0No, plain text linesYes
EasyOCR 1.7.2Large (pulls in PyTorch)Several: $ read as S or 8, two numbers missedNo, one string per boxYes
PaddleOCR 3.4.0Large (pulls in PaddlePaddle)0, plus one stray ANo, one string per boxYes
OCR API (mistral/mistral-ocr)Just requests0Yes, Markdown tableNo
  • Pick pytesseract for clean, printed scans when you want a small install and no network.
  • Pick PaddleOCR when you also need Chinese, Japanese or other non-Latin scripts and can afford a large install.
  • Pick an API when you need tables, handwriting or messy phone photos, or you do not want to manage models. The handwriting and table posts show where local engines fall short.

One invoice is not a benchmark. Treat the table as "this is what the code does", not as a ranking. For rankings, see our post on OCR benchmarks.

Set up a clean environment

Use a virtual environment so the heavy dependencies do not fight with each other. We used Python 3.12 on an Apple Silicon Mac.

python3.12 -m venv .venv
source .venv/bin/activate
pip install pytesseract pillow requests

pytesseract does not include the OCR engine. Install Tesseract itself from your package manager:

# macOS
brew install tesseract
# Debian or Ubuntu
sudo apt install tesseract-ocr

Check it with tesseract --version. We used 5.5.4.

Option 1: pytesseract

pytesseract calls the tesseract command for you and returns its output as a Python string.

from PIL import Image
import pytesseract

text = pytesseract.image_to_string(Image.open("invoice.png"), lang="eng")
print(text)

Output on our invoice (first lines):

Invoice INV-2041

Acme Supply Co.
14 Harbour Street, Sydney NSW 2000
Issued 3 November 2026. Due 17 November 2026.

Item Qty Unit price Amount
A4 paper, 5 reams 4 $24.00 $96.00
Toner cartridge 2 $89.50 $179.00

Every word was right. The table came back as space-separated lines, so you would have to split columns yourself.

Get word positions and confidence

image_to_data returns one row per word with its bounding box and a confidence score from 0 to 100:

from PIL import Image
import pytesseract

data = pytesseract.image_to_data(Image.open("invoice.png"), output_type=pytesseract.Output.DICT)
for word, conf in zip(data["text"], data["conf"]):
    if word.strip():
        print(f"{word:<12} {conf}")

Most words scored 95 or 96. The lowest were A4 at 77 and Item at 88, and both were still read correctly. A low score tells you where to look, not that the word is wrong.

When pytesseract struggles

Tesseract wants clean input. Its own documentation says it "works best on images which have a DPI of at least 300 dpi". In our tests it read every word of the invoice at full size, but only 6.9% of the words when we shrank the image to a third (333 x 253 px), and 6.4% of the words in a handwritten note. Details are in How to improve OCR accuracy.

Option 2: EasyOCR

EasyOCR is a deep-learning OCR library built on PyTorch. It downloads its detection and recognition models the first time you run it.

pip install easyocr
import easyocr

reader = easyocr.Reader(["en"], gpu=False)  # downloads models on first run
for bbox, text, conf in reader.readtext("invoice.png"):
    print(f"{conf:.2f}  {text}")

What we got back (excerpt):

0.97  Invoice INV-2041
0.69  Acme Supply Co.
0.67  14 Harbour Street; Sydney NSW 2000
0.94  S24.00
0.62  S96.00
0.56  815.00
0.81  Oty

EasyOCR found every line, but it read most dollar signs as S, once as 8, turned commas into semicolons, read Qty as Oty, and skipped the single-digit quantities. On printed English, Tesseract did better here.

A dependency trap we hit

In a fresh environment, pip resolved PyTorch 2.2.2 together with NumPy 2.x, and readtext failed with RuntimeError: Numpy is not available. Pinning NumPy fixed it:

pip install easyocr "numpy<2"

If you install EasyOCR and PaddleOCR in the same environment you are more likely to hit conflicts like this. Give each its own venv.

Option 3: PaddleOCR

PaddleOCR is Baidu's open-source OCR toolkit. Version 3 changed the API: you call predict() and read rec_texts and rec_scores from each result. Older tutorials that call ocr.ocr(img, cls=True) are for version 2.

pip install paddlepaddle paddleocr
from paddleocr import PaddleOCR

ocr = PaddleOCR(
    lang="en",
    use_doc_orientation_classify=False,
    use_doc_unwarping=False,
    use_textline_orientation=False,
)
for page in ocr.predict("invoice.png"):
    for text, score in zip(page["rec_texts"], page["rec_scores"]):
        print(f"{score:.2f}  {text}")

We turned off the three optional pre-processing models because the invoice is a flat, upright scan; leave them on for phone photos. The first run downloads models; on our laptop the whole script took about 19 seconds on CPU, most of it loading.

Every word was correct, with scores from 0.93 to 1.00. It also returned one stray A with a score of 1.00, so a high score does not prove a line is real. Like EasyOCR, it returns one string per detected box, so table cells come back as separate lines: Item, Qty, Unit price, Amount.

Option 4: call an OCR API from Python

A hosted API runs the model for you. You send the file, you get text back. With OCRRouter the request is the same for every model, so you can switch from one engine to another by changing one string.

import base64, os, requests

API = os.environ.get("OCRROUTER_API", "https://api.ocrrouter.ai")

with open("invoice.png", "rb") as f:
    b64 = base64.b64encode(f.read()).decode()

r = requests.post(
    f"{API}/v1/ocr",
    headers={"Authorization": f"Bearer {os.environ['OCRROUTER_API_KEY']}"},
    json={"model": "mistral/mistral-ocr", "file_base64": b64, "format": "markdown"},
    timeout=120,
)
r.raise_for_status()
print(r.json()["markdown"])

Output (excerpt):

# Invoice INV-2041

Acme Supply Co.

| Item | Qty | Unit price | Amount |
| --- | --- | --- | --- |
| A4 paper, 5 reams | 4 | $24.00 | $96.00 |
| Toner cartridge | 2 | $89.50 | $179.00 |
| Delivery | 1 | $15.00 | $15.00 |

The difference from the local libraries is the structure: the line items came back as a Markdown table you can parse, and the title as a heading. If your file is online, send "file_url" instead of "file_base64". Ask for "format": "json" to get typed blocks, including tables as lists of rows.

We ran the same invoice through eight models on our API. Five kept the line items as a table: mistral/mistral-ocr, zai/glm-ocr, paddle/paddleocr-vl, jina/jina-ocr and azure/document-intelligence. Three returned them as plain text lines: google/document-ai, aws/textract and tesseract/tesseract. Prices per page are on each model page.

OCR on a PDF in Python

All three local libraries take images, not PDFs. Render each page to an image first. PyMuPDF does this without any system dependencies:

import pymupdf  # pip install pymupdf

doc = pymupdf.open("scan.pdf")
for i, page in enumerate(doc):
    page.get_pixmap(dpi=300).save(f"page-{i + 1}.png")

Use 300 dpi to match Tesseract's guidance. If the PDF already has a text layer (it was exported, not scanned), you do not need OCR at all: page.get_text() returns the text directly. The OCRRouter API takes PDFs as they are, and you can limit it to some pages with "pages": "1-5".

Common errors and fixes

  • TesseractNotFoundError: pytesseract cannot find the binary. Install Tesseract, or set pytesseract.pytesseract.tesseract_cmd to its full path.
  • RuntimeError: Numpy is not available in EasyOCR: your PyTorch build predates NumPy 2. Install "numpy<2" or a newer PyTorch.
  • AttributeError or unexpected result format in PaddleOCR: you are following a 2.x tutorial on 3.x. Use predict() and the rec_texts key.
  • Garbage output from any engine: check the image size first. On an invoice shrunk to a third of its size, Tesseract read 6.9% of the words while the seven API models we tried read 89.7% to 100%. The accuracy guide has before-and-after numbers.

FAQ

Read next