Fix tabula-py Returns Empty DataFrame

tabula runs without an error and returns nothing useful:

>>> import tabula
>>> tabula.read_pdf("in/remittance.pdf")
[]
>>> tabula.read_pdf("in/remittance.pdf", pages="all")
[Empty DataFrame
Columns: [Unnamed: 0]
Index: []]

The table is visible in any PDF viewer — bordered on some pages, borderless on others — and pdfplumber or camelot can see text on the same pages. No warning explains why tabula found nothing.

Root Cause

tabula-py is a wrapper around tabula-java, and several of its defaults quietly narrow what it looks at. read_pdf reads only page 1 unless pages is given, so tables on later pages are never examined. With the default guess=True, tabula first runs its own table detection and extracts only from regions it detected; on borderless tables and busy pages detection often finds nothing, and extraction then returns an empty list. When you pass an area, it is ignored while guess=True is still on. Lattice mode (lattice=True) only builds cells from drawn ruling lines, so a borderless table yields empty frames; stream mode without lines needs whitespace between columns and can collapse a table into a single Unnamed: 0 column with no rows if the area is wrong. And tabula reads text, so scanned pages produce nothing in any mode. Java errors are a separate failure — those raise rather than return empty results.

Minimal Diagnostic

Probe the file systematically: confirm there is text on each page, then run tabula with each mode and guessing on and off, and print the shapes it returns.

# pip install tabula-py pdfplumber
from pathlib import Path
import pdfplumber
import tabula

SOURCE = Path("in/remittance.pdf")

def probe(pdf_path: Path) -> None:
    with pdfplumber.open(pdf_path) as pdf:
        for i, page in enumerate(pdf.pages, start=1):
            chars = len(page.chars)
            lines = len(page.lines) + len(page.rects)
            print(f"page {i}: {chars} chars, {lines} ruling lines/rects")
    combos = [dict(), dict(pages="all"), dict(pages="all", guess=False),
              dict(pages="all", lattice=True), dict(pages="all", stream=True, guess=False)]
    for kwargs in combos:
        try:
            frames = tabula.read_pdf(str(pdf_path), multiple_tables=True, **kwargs)
            shapes = [f.shape for f in frames]
        except Exception as exc:
            shapes = f"ERROR {type(exc).__name__}: {exc}"
        print(f"{kwargs or 'defaults'}: {shapes}")

if __name__ == "__main__":
    probe(SOURCE)
page 1: 212 chars, 0 ruling lines/rects
page 2: 1830 chars, 0 ruling lines/rects
page 3: 1644 chars, 48 ruling lines/rects
defaults: []
{'pages': 'all'}: [(0, 1)]
{'pages': 'all', 'guess': False}: [(3, 2), (41, 6), (37, 6)]
{'pages': 'all', 'lattice': True}: [(18, 6)]
{'pages': 'all', 'stream': True, 'guess': False}: [(3, 2), (41, 6), (37, 6)]

Page 1 is a cover letter; the table starts on page 2 and is borderless there, bordered on page 3. Defaults look only at page 1. Guessing finds nothing on the borderless page. Lattice sees only the bordered page. Stream without guessing finds data on every page.

What each tabula setting returned With defaults tabula read only page 1, the cover letter, and returned an empty list. With pages all and guessing on, detection failed and returned one empty frame. With pages all and guess False, tabula returned frames for all three pages including the cover letter as a small frame. Lattice mode returned a table only for the bordered third page. Stream mode with guess False returned the tables on pages two and three plus the cover page fragment. Setting Page 1 cover Page 2 borderless Page 3 bordered defaults read not read not read pages=all empty guess fails guess fails pages=all, guess=False 2 junk rows 41 rows 37 rows lattice=True nothing no lines 18 rows stream, guess=False 2 junk rows 41 rows 37 rows

Fix: Read the Right Pages, in the Right Mode, Without Guessing

Specify pages explicitly, choose stream or lattice per page from whether ruling lines exist, and turn guessing off whenever you supply an area or know the layout. Changed lines carry comments.

# pip install tabula-py pdfplumber "pandas>=2.2"
from pathlib import Path
import pandas as pd
import pdfplumber
import tabula

SOURCE = Path("in/remittance.pdf")
HEADER = ["Invoice", "Date", "Gross", "Discount", "Paid", "Balance"]

def page_modes(pdf_path: Path, min_chars: int = 300) -> dict[int, str]:
    modes = {}
    with pdfplumber.open(pdf_path) as pdf:
        for i, page in enumerate(pdf.pages, start=1):
            if len(page.chars) < min_chars:
                continue                                                     # changed: skip cover/blank pages
            modes[i] = "lattice" if len(page.lines) + len(page.rects) > 10 else "stream"  # changed: per page
    return modes

def read_tables(pdf_path: Path) -> pd.DataFrame:
    frames = []
    for page, mode in page_modes(pdf_path).items():
        try:
            found = tabula.read_pdf(
                str(pdf_path),
                pages=page,                                                  # changed: explicit page
                guess=False,                                                 # changed: no detection step
                lattice=(mode == "lattice"),
                stream=(mode == "stream"),
                multiple_tables=True,
                pandas_options={"header": None, "dtype": str},               # changed: keep raw text
            )
        except Exception as exc:
            raise RuntimeError(f"tabula failed on page {page}: {exc}") from exc
        for frame in found:
            frame = frame.dropna(how="all")
            if frame.shape[1] == len(HEADER):
                frames.append(frame.assign(page=page))
    if not frames:
        raise ValueError("no table with the expected column count on any page")
    combined = pd.concat(frames, ignore_index=True)
    combined.columns = [*HEADER, "page"]
    return combined[combined["Invoice"].str.match(r"^INV-\d+", na=False)]   # changed: drop header rows

if __name__ == "__main__":
    print(read_tables(SOURCE).head())

pandas_options={"header": None} stops tabula from promoting the first row — often a title line or a fragment — to column names, which is where many Unnamed: 0 frames come from. Filtering by the expected column count and an invoice-number pattern discards the cover letter fragment and repeated headers in one pass. Reading everything as strings keeps amounts like 1,240.50 intact for explicit parsing later.

Variant Fix 1: Restrict to an Area — With Guessing Off

Busy pages where stream mode picks up the letterhead or footer benefit from an explicit area. tabula ignores area unless guess=False:

# pip install tabula-py
import tabula

frames = tabula.read_pdf(
    "in/remittance.pdf",
    pages=2,
    area=[160, 30, 760, 565],          # top, left, bottom, right in points from the top-left
    guess=False,                        # required, or the area is silently ignored
    stream=True,
    columns=[95, 170, 270, 350, 440],  # optional x boundaries between columns
    pandas_options={"header": None, "dtype": str},
)
print([f.shape for f in frames])

area values can also be percentages of the page with relative_area=True, which helps when the same layout is printed on A4 and Letter. Measuring the box from anchor text rather than by eye is covered in extract a PDF table by bounding box coordinates.

Reading a mixed document with tabula Each page is profiled with pdfplumber for character count and ruling lines. Pages with little text, such as cover letters, are skipped. Pages with many ruling lines use lattice mode and the rest use stream mode, always with guess False. Returned frames are filtered by expected column count and a key pattern, then concatenated. A branch shows scanned pages with no characters going to OCR first. Profile page chars and lines Skip sparse cover or blank Pick mode lattice or stream guess=False explicit page Filter frames shape and key 0 chars scanned, OCR first

Variant Fix 2: Scanned Pages and Encoding Problems

A page with zero characters in the diagnostic is an image. tabula cannot read it in any mode; add a text layer with OCR first, then run the same code:

# pip install ocrmypdf tabula-py
from pathlib import Path
import ocrmypdf

def ocr_then_tabula(src: Path, work: Path):
    work.mkdir(parents=True, exist_ok=True)
    ocred = work / src.name
    ocrmypdf.ocr(src, ocred, skip_text=True, progress_bar=False)
    return read_tables(ocred)                      # the fix function above

When pages have characters but tabula returns frames full of mojibake or empty strings, the problem is text encoding rather than detection: tabula-java needs the correct encoding for its output. Pass encoding="utf-8" (the default) or, on Windows systems with a legacy default charset, java_options=["-Dfile.encoding=UTF8"] so accented text survives the handoff from Java. If the characters themselves decode as (cid:..) in every library, see fix CID garbled characters in PDF text.

Keeping tabula Fast in Batch Jobs

Every read_pdf call starts a Java virtual machine unless tabula-py is told otherwise, which adds about a second per call — the per-page loop above multiplies it. Recent tabula-py versions can reuse one JVM in-process through jpype, and batch conversion of many files in one call avoids repeated start-up:

# pip install "tabula-py[jpype]"
import time
import tabula

start = time.perf_counter()
for page in range(2, 12):
    tabula.read_pdf("in/remittance.pdf", pages=page, guess=False, stream=True,
                    force_subprocess=False)             # use the in-process JVM when jpype is installed
print(f"10 calls: {time.perf_counter() - start:.1f}s")

# whole folder in one Java invocation, writing CSV next to each PDF
tabula.convert_into_by_batch("in/remittances", output_format="csv", pages="all",
                             guess=False, stream=True)
Time for ten single-page tabula calls Ten single-page read_pdf calls took 13.8 seconds when each call started a Java subprocess, 2.1 seconds when tabula-py reused one in-process JVM through jpype, and 1.6 seconds when the same pages were converted in a single batch call. JVM start-up dominates the subprocess case. remittance.pdf, pages 2 to 11, stream mode, guess=False Subprocess per call 13.8 s In-process JVM (jpype) 2.1 s One batch call 1.6 s Most of the subprocess time is starting Java, not reading the PDF

With jpype installed, the JVM starts once and later calls take milliseconds of overhead. convert_into_by_batch is convenient for exports but gives less control over per-page mode choice; use it only for folders of uniformly formatted files. When Java is absent altogether, calls raise instead of returning empty, as described in fix tabula java not found error.

Verification

Assert non-empty output with the expected columns, compare the row count with a control total on the document, and check that amounts parse.

# pip install "pandas>=2.2" pdfplumber
import re
from pathlib import Path
import pandas as pd
import pdfplumber

def verify_remittance(pdf_path: Path, table: pd.DataFrame) -> None:
    assert not table.empty, "tabula returned no rows"
    amounts = pd.to_numeric(table["Paid"].str.replace(",", ""), errors="coerce")
    assert amounts.notna().all(), f"unparseable Paid values: {table.loc[amounts.isna(), 'Paid'].tolist()[:3]}"
    with pdfplumber.open(pdf_path) as pdf:
        text = "\n".join(p.extract_text() or "" for p in pdf.pages)
    total = re.search(r"Total paid\s*[£€$]?\s*([\d,]+\.\d{2})", text)
    if total:
        expected = float(total.group(1).replace(",", ""))
        assert abs(amounts.sum() - expected) < 0.01, f"rows sum to {amounts.sum():.2f}, document says {expected:.2f}"
    print(f"{len(table)} rows across pages {sorted(table['page'].unique())}; total {amounts.sum():,.2f}")

A remittance's printed total is the best available check: an empty page, a dropped row or a shifted column all change the sum.

FAQ

Why does tabula's GUI find the table when the Python call does not? In the GUI you draw an area, which is equivalent to area=... with guess=False. Use the GUI's exported template JSON with tabula.read_pdf_with_template to reuse those exact areas.

Why do I get one column named Unnamed: 0 and no rows? tabula found a single text block and pandas promoted its first line to the header, leaving an empty body. It usually means the area covers only a title or the page is the wrong one. Pass pandas_options={"header": None} to see the raw rows, then fix the page or area.

Can tabula read password-protected PDFs? Pass password="...". Without it, encrypted files raise an error or return nothing depending on the encryption type, so decrypt first when in doubt.

Should I always set guess=False? When you know the pages and layout, yes. Guessing helps for exploratory runs on unfamiliar documents, not in production pipelines.

Why does multiple_tables=False return one huge frame? It concatenates everything tabula finds on the selected pages into one frame. Keep multiple_tables=True and filter, so unrelated fragments do not pollute the table.

Is tabula still worth using over camelot or pdfplumber? It is good at stream-mode tables and handles large batches efficiently. The trade-offs are in pdfplumber vs camelot vs tabula.

Part of Comparing PDF Table Extraction Libraries.