Independent guide. Not affiliated with the pdfplumber project or its maintainers.Official repository: jsvine/pdfplumber ↗

Compare

pdfplumber vs Camelot

Camelot does one thing, tables, and has grown several ways of finding them. pdfplumber gives you the whole page and a table finder you steer yourself. We ran both on three tables whose correct contents we know, with default settings and with one targeted adjustment each.

Tested with pdfplumber 0.11.10 · Python 3.13Updated 2026-11-04

Short answer

pdfplumber 0.11.10 Camelot 2.0.0
Ruled table, defaults Exact Exact (lattice)
Rows ruled, no column lines, defaults No table found All rows, plus page lines (network)
Borderless table, defaults No table found All rows, plus title lines (stream, network)
Each of the three after one adjustment Exact Exact
Also extracts text, words, objects Yes No: tables only
Install size (Linux, Python 3.13) 58.7 MB 356.3 MB (OpenCV, pandas, NumPy)
Licence MIT MIT

Use Camelot when tables are the whole job and you want good results without tuning. Use pdfplumber when you also need the text around the tables, want to see and control exactly how cells are found, or care about install size.

All examples below check their output against the data the sample PDFs were generated from, published next to them (invoice, bank statement, borderless table), so “exact” means every cell of every row matches.

Camelot in 2026: six flavors and a new parser

Camelot calls its strategies flavors. Version 2.0.0, released on 4 June 2026, has six: lattice (ruled tables, from a rendered image of the page), stream (columns from whitespace), network (columns from text alignment) and hybrid (a mix of layout and image analysis), both added in 1.0,, ml (a Table Transformer model, as an optional extra) and auto, which picks lattice or network per page. lattice is the default.

2.0 also changed what is underneath. Camelot 1.x parsed PDFs with pdfminer.six, the same parser as pdfplumber, and pinned a newer version than older pdfplumber releases allowed:

$ pip install --dry-run "pdfplumber==0.11.5" "camelot-py==1.0.9"
ERROR: Cannot install camelot-py==1.0.9 and pdfplumber==0.11.5 because these package versions have conflicting dependencies.

Camelot 2.0 uses the playa-pdf parser instead, so current versions of both install side by side; we checked with pip’s resolver. Camelot 2.0 needs Python 3.10 or newer.

Ruled tables: both exact

ruled_table.py
import json
import time

import camelot
import pdfplumber

truth = json.load(open("samples/truth/invoice.json"))["table"]

t = time.perf_counter()
with pdfplumber.open("samples/invoice.pdf") as pdf:
    plumber = pdf.pages[0].extract_table()
t_plumber = time.perf_counter() - t

t = time.perf_counter()
tables = camelot.read_pdf("samples/invoice.pdf", pages="1", flavor="lattice")
t_camelot = time.perf_counter() - t
lattice = tables[0].df.values.tolist()

print(f"pdfplumber        {len(plumber)} rows, matches truth: {plumber == truth}, {t_plumber:.2f} s")
print(f"camelot lattice   {len(lattice)} rows, matches truth: {lattice == truth}, {t_camelot:.2f} s")
print("camelot's own report:", tables[0].parsing_report)
Outputcaptured · Python 3.13.18 · pdfplumber 0.11.10
pdfplumber        8 rows, matches truth: True, 0.02 s
camelot lattice   8 rows, matches truth: True, 1.58 s
camelot's own report: {'accuracy': 100.0, 'whitespace': 0.0, 'order': 1, 'page': 1, 'confidence': 1.0}

Both get the invoice’s line items exactly. Camelot’s lattice is much slower, as the timings show, because it renders the page to an image and finds the lines with OpenCV; pdfplumber reads the lines directly from the PDF. Camelot also returns a parsing report, its own estimate of how well the extraction went, which pdfplumber has no equivalent of. It is useful for flagging pages to review in a batch.

Ruled rows, no column lines

The bank statement draws a line under each row but none between columns. This is where defaults differ most:

bank_statement.py
import json

import camelot
import pdfplumber

truth = json.load(open("samples/truth/bank-statement.json"))
expected = [truth["header"]] + truth["rows"][:22]            # page 1: header + 22 rows

def score(rows):
    return f"{len(rows):>2} rows, {sum(r in expected for r in rows):>2}/{len(expected)} match"

with pdfplumber.open("samples/bank-statement.pdf") as pdf:
    page = pdf.pages[0]
    print("pdfplumber defaults      ", page.find_tables() and "found" or "no table")
    names = truth["header"]
    head = [w for w in page.extract_words() if w["text"] in names]
    cols = [head[0]["x0"] - 4] + [(a["x1"] + b["x0"]) / 2 for a, b in zip(head, head[1:])] + [head[-1]["x1"] + 4]
    rows = page.extract_table({"vertical_strategy": "explicit", "explicit_vertical_lines": cols,
                               "horizontal_strategy": "lines"})
    print("pdfplumber, header cols  ", score(rows))

for flavor in ["stream", "network"]:
    tables = camelot.read_pdf("samples/bank-statement.pdf", pages="1", flavor=flavor)
    biggest = max((t.df.values.tolist() for t in tables), key=len)
    print(f"camelot {flavor:8} defaults ", score(biggest))

tables = camelot.read_pdf("samples/bank-statement.pdf", pages="1", flavor="stream",
                          table_areas=["40,694,560,276"])      # around the table, in PDF points
print("camelot stream, area     ", score(tables[0].df.values.tolist()))
Outputcaptured · Python 3.13.18 · pdfplumber 0.11.10
pdfplumber defaults       no table
pdfplumber, header cols   23 rows, 23/23 match
camelot stream   defaults  24 rows,  0/23 match
camelot network  defaults  27 rows, 23/23 match
camelot stream, area      23 rows, 23/23 match
  • pdfplumber’s default lines strategy needs vertical lines too, so it finds no table. Taking the column positions from the header words, as in the tutorial, makes it exact.
  • Camelot’s stream defaults merged columns and matched no row. network found every row with no settings at all, plus the four lines of page heading above it (bank name, statement period, account holder, opening balance), which you would drop.
  • Telling Camelot where the table is (table_areas) makes stream exact as well.

This is the case for Camelot: network deals with partly ruled tables without being told anything.

Borderless tables

borderless_table.py
import json

import camelot

truth = json.load(open("samples/truth/borderless-table.json"))["table"]

# Default stream: the whole page becomes one table, title lines included
whole = camelot.read_pdf("samples/borderless-table.pdf", pages="1", flavor="stream")[0].df.values.tolist()
print(len(whole), "rows; first two:", whole[:2])

# Limit the search to the table's area (PDF points, origin bottom-left: x1,y1,x2,y2)
area = camelot.read_pdf("samples/borderless-table.pdf", pages="1", flavor="stream",
                        table_areas=["50,674,590,542"])[0].df.values.tolist()
print(len(area), "rows; matches truth:", area == truth)
Outputcaptured · Python 3.13.18 · pdfplumber 0.11.10
10 rows; first two: [['Quarterly operating results (fictional)', '', '', '', ''], ['In thousands of US dollars, except percentages', '', '', '', '']]
7 rows; matches truth: True

Camelot’s stream takes in the whole page as one table, title and subtitle included; limited to the table’s area, it is exact. pdfplumber’s defaults find nothing on this page, and the recipe in borderless tables, which places column lines in the gaps between words, is exact too.

One thing to watch with Camelot: table_areas and columns use PDF coordinates, with the origin at the bottom-left of the page. pdfplumber measures top from the top edge. To convert a pdfplumber box to a Camelot area: x0, page.height - top, x1, page.height - bottom.

Speed and size

Seconds per table, ruled invoice, default settings · shorter is better
  • pdfplumber0.03 s
  • camelot stream0.02 s
  • camelot lattice0.33 s

Source: our benchmark, 2026-11-04, Intel(R) Xeon(R) Processor @ 2.10GHz; median of repeated runs, after imports; script lab/bench/run_bench.py

stream is quick because it only reads text; lattice renders the page. Installed, Camelot 2.0 with its dependencies took 356.3 MB in our CI run, most of it OpenCV (159.5 MB) and pandas; pdfplumber took 58.7 MB. In a Docker image or a serverless function, that difference matters.

Which to use

  • Tables are the whole job, layouts vary, no time to tune: Camelot, starting with auto or network. Use the parsing report to find pages that need a look.
  • You also need text, fields, positions or fonts: pdfplumber; one library for the whole page. See extract text and words, chars and fonts.
  • You want to see why a table failed: pdfplumber’s debug_tablefinder() draws every edge and intersection it used. Camelot can plot its detections too, with matplotlib installed, but pdfplumber’s settings map more directly onto what you see.
  • Small installs (Lambda, containers): pdfplumber.

Many projects combine them: pdfplumber to find the pages and regions that hold tables, Camelot to read the difficult ones. tabula-py is the third option in this space, with its own trade-offs.