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.
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
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)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:
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()))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
linesstrategy 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
streamdefaults merged columns and matched no row.networkfound 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) makesstreamexact as well.
This is the case for Camelot: network deals with partly ruled tables without being told anything.
Borderless tables
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)10 rows; first two: [['Quarterly operating results (fictional)', '', '', '', ''], ['In thousands of US dollars, except percentages', '', '', '', '']]
7 rows; matches truth: TrueCamelot’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
- 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
autoornetwork. 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.