Camelot lattice vs stream: lattice needs every line
0 of 139
tables with no full grid, found by lattice
Try it on your PDF, $0.006 a page
Grid: lattice with split_text. Else: stream
92 of 162 hybrid, split_text: best Camelot setting
80 of 162 stream
| Flavor | Full grid (23) | No full grid (139) |
|---|---|---|
| lattice | 14 | 0 |
| lattice, split_text | 22 | 0 |
| stream | 11 | 69 |
| stream, split_text | 11 | 71 |
| network | 10 | 69 |
| hybrid | 14 | 69 |
| hybrid, split_text | 22 | 70 |
"No full grid" means a box only, rules under some rows, stripes, or no lines. Not sure which you have? Use hybrid with split_text.
The default call returns an empty list
import camelot
tables = camelot.read_pdf("statement.pdf")
print(tables)
tables = camelot.read_pdf("statement.pdf", flavor="stream", pages="all")
for row in tables[0].df.values.tolist()[:2]:
print(row)
<TableList n=0>
['Test page', '', '', '', '']
['Date', 'Description', 'Withdrawals', 'Deposits', 'Balance']
- No error and no warning. The default flavor is lattice, and this bank statement has no lines.
-
pagesdefaults to "1". On a two-page PDF, stream gave one table, andpages="all"gave both.
Grid table wrong? Add split_text=True
import camelot
for split in (False, True):
table = camelot.read_pdf("grid.pdf", split_text=split)[0]
print(table.df.values.tolist()[0], table.parsing_report["accuracy"])
['Date', 'Description', '', 'Withdrawals Deposits Balance', ''] 68.97
['Date', 'Description', 'Withdrawals', 'Deposits', 'Balance'] 68.97
8 of 9 lattice grid misses fixed
0 right tables made wrong
One line of text crossed 3 cells, and lattice put it all in one cell.
split_text=True cuts it at the cell walls.
The accuracy score missed wrong tables
20 of 79 wrong hybrid tables scored 100
29 of 82 wrong stream tables scored 99 or more
parsing_report["accuracy"] did not tell right tables from
wrong ones, and the fix above did not move it. Look at a few cells
before you trust a table.
Our Actor: 59 of 78 exact, held out
87.9% of cells right, our Actor
75.3% hybrid, split_text
Build 0.1.32 finds columns from the strips that no word crosses. The old build got fewer than stream.
Our code was tuned on 2 earlier sets from the same generator, so this is near its best case. Camelot had no tuning.
Scanned PDF? Lattice and stream read nothing
page-1 is image-based, camelot only works on text-based pages.
Lattice, stream and hybrid all gave that warning on an invoice scan, and no table. A scan needs OCR first. See Tesseract table extraction.
Tables in your PDFs? Try our Actor
PDF OCR to Markdown, our Apify Actor, turns each page into Markdown, with tables as Markdown tables.
- Digital pages use the PDF's own text. Scans get Tesseract OCR.
- Your files go to no LLM or outside API.
- $0.006 a page, scanned or digital, plus $0.00005 a run at the default 1 GB.
- The Apify free plan lets you try 20 pages a run and 50 a month.
Questions people ask
Does flavor="auto" pick the right one?
Not always. On the held-out set, 8 tables had only a box around them. It chose lattice for all 8 and gave no table.
Does Camelot need Ghostscript?
Not in version 2.0.0. Lattice draws the page with pdfium by default, and every run here had no Ghostscript on the PATH.
Is lattice slower than stream?
Yes. The median was 133 ms a page for lattice and 7 ms for stream. Lattice draws the page as an image to find the lines.
What about flavor="ml"?
I did not test it. It needs the camelot-py[ml] extra and a Table Transformer model. Its source says it fills cells from the PDF's own text, and can OCR a scan with the camelot-py[ocr] extra.
Camelot or pdfplumber?
On the PDFs of our pdfplumber guide, Camelot stream got half the tables exact, and pdfplumber's default got 12 of 84. A column recipe for pdfplumber got all of them, but it was tuned on those PDFs.
Method, limits and sources
How I tested it
156 made-up one-page PDFs with a text layer. 84 are the invoices, statements, parts lists and payrolls of our pdfplumber guide, written by Chromium, LibreOffice and ReportLab.
72 are a held-out set with 78 tables. A separate generator varied fonts, gaps, wrapping and lines. It was made after our Actor's table code froze.
Our Actor's table code was also tuned on the 84 PDFs.
Each flavor ran with pages="all" and default
settings, plus split_text where named. Exact means every cell
matches, with spaces made single, and no extra column. Extra rows
of page text are allowed.
Scripts and results are in research/bench/camelot-2026-11-02/. The true tables are in the truth files of research/bench/pdfplumber-tables-2026-11-02/ and research/bench/pdf-gridless-2026-11-02/.
What I did not test
- Real PDFs. These are all made up.
- flavor="ml", table_areas, columns and row_tol.
- Tables that run over 2 pages.
- Linux and Windows.
Versions and sources
- camelot-py 2.0.0 (MIT), pdfminer.six 20260107.
- pypdfium2 5.14.0, OpenCV 5.0.0, Python 3.12.
- A Mac, November 2, 2026.
- camelot-py 2.0.0 on PyPI
- Camelot docs
- We make the Apify Actor. Camelot, pdfminer.six and pdfium are open source tools we do not make.