All posts
Engineering8 min read

How to extract text from an image using Python

Tesseract via pytesseract, EasyOCR, PaddleOCR, and the API-based path — what each one is best at, what they break on, and the few lines of code to get started.

By · Updated

OCR in Python is mature but uneven. The right library depends on the language, the image quality, and whether you care about layout preservation or just raw text.

Tesseract via pytesseract

The default. Free, well-supported, handles 100+ languages. Install Tesseract at the OS level (brew install tesseract or apt install tesseract-ocr), then:

import pytesseract from PIL import Image text = pytesseract.image_to_string(Image.open("page.png"), lang="eng") print(text)

For better results on scans, preprocess: convert to grayscale, threshold, deskew. The OpenCV docs have one-liners for each. Skipping preprocessing on noisy scans is the most common reason Tesseract output looks broken.

Strengths: clean printed English, large fonts, structured documents. Weaknesses: handwritten text, low-res screenshots, anything with mixed fonts in the same line.

Preprocessing with OpenCV

The difference preprocessing makes on a phone-photo scan is dramatic. A minimal pipeline:

import cv2 img = cv2.imread("scan.jpg", cv2.IMREAD_GRAYSCALE) img = cv2.threshold(img, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)[1] text = pytesseract.image_to_string(img)

Grayscale plus Otsu thresholding handles most contrast problems. Add cv2.medianBlur for speckle noise, and rotate to deskew when the lines aren't horizontal — Tesseract silently degrades on text tilted more than a degree or two.

Word positions and confidence: image_to_data

image_to_string returns one blob of text. image_to_data returns a table — every detected word with its bounding box, line number, and confidence score:

data = pytesseract.image_to_data(img, output_type=pytesseract.Output.DICT) for i, word in enumerate(data["text"]): if word.strip() and int(data["conf"][i]) > 60: print(word, data["conf"][i])

This is the hook for building key-value extraction yourself: filter by confidence, group words by line, and locate values relative to their labels. One more knob worth knowing is page segmentation mode — the default assumes a full page of text, so for a cropped field or single line pass --psm 7 (single line) or --psm 6 (uniform block). Wrong PSM is a common cause of empty output on small crops.

EasyOCR

Deep-learning-based, pip-installable, no system dependencies. Better than Tesseract on natural-scene text (signs, receipts, labels) and on non-Latin scripts.

import easyocr reader = easyocr.Reader(["en"]) results = reader.readtext("receipt.jpg") for bbox, text, conf in results: print(text, conf)

Returns bounding boxes and confidence scores per detection — useful when you need to filter low-confidence reads. Slower than Tesseract on first run because it downloads model weights.

PaddleOCR

From Baidu. Strongest on Chinese, Japanese, Korean. Comparable to EasyOCR on English. Heavier install but worth it if you're processing CJK documents at scale.

Handwriting

None of the open-source OCR engines handle handwriting well. For handwritten text, the realistic options are Microsoft's Read API (Computer Vision), Google Cloud Vision, or a multimodal model like the one ExtractFox uses for the handwriting extractor.

API-based extraction

When you need not just text but structure (fields, tables, key-value pairs), the multimodal route skips the OCR-then-parse pipeline entirely. You send the image, declare what you want as a schema, and get structured output back:

# Create an API key at extractfox.com/developers. import base64 import requests API_KEY = "efk_live_..." with open("invoice.jpg", "rb") as f: data = base64.b64encode(f.read()).decode() r = requests.post( "https://extractfox.com/api/v1/extractions", json={ "mode": "prebuilt", "schema_id": "invoice", "file": {"data": data, "media_type": "image/jpeg", "name": "invoice.jpg"}, }, headers={"Authorization": f"Bearer {API_KEY}"}, ) result = r.json()["result"]

Worth it when extraction quality matters more than running locally for free.

Batch: a folder of images

from pathlib import Path import pytesseract for f in sorted(Path("scans").glob("*.png")): text = pytesseract.image_to_string(str(f)) f.with_suffix(".txt").write_text(text)

One .txt next to every .png. For thousands of images, wrap the loop in multiprocessing.Pool — OCR is CPU-bound and parallelizes nearly linearly across cores.

Common errors and fixes

  • TesseractNotFoundError — the binary isn't on PATH. Install it at the OS level, or set pytesseract.pytesseract.tesseract_cmd to the full path.
  • Error opening data file eng.traineddata — the language pack is missing. Install tesseract-ocr-eng (or your language), or point the TESSDATA_PREFIX environment variable at the tessdata directory.
  • Garbage output on a clean-looking scan — check the resolution; Tesseract wants roughly 300 DPI. Upscale small images with cv2.resize before OCR.
  • Digits read as letters (O vs 0, l vs 1) — restrict the character set with -c tessedit_char_whitelist=0123456789 for numeric-only fields.

For mixed-language documents, pass multiple language codes at once — lang="eng+deu" — but expect a speed and accuracy penalty per extra language. If you know the document's language upfront, always say so explicitly.

Choosing

  • Clean printed English, no internet → Tesseract.
  • Receipts, signs, multilingual → EasyOCR.
  • CJK at scale → PaddleOCR.
  • Handwriting → Cloud Vision or multimodal.
  • Need structured fields, not just text → API-based.

Related reading

Stop reading, start extracting

Drop a PDF or image into ExtractFox and get structured data back in seconds.

Try a free extraction →