If your PDFs are digital, consistent, and you are comfortable maintaining a bit of code, Python extracts their tables well and for free. Three libraries do almost all of this work: pdfplumber, camelot, and tabula-py. Each takes a different approach, each has a case it handles best, and all three share the same blind spot around scanned documents. This is a practical tour of the three, the minimal code for each, and an honest read on when writing and owning a script is worth it versus when it is not.
The build versus buy question is the real subject here. A one-off extraction from ten clean PDFs is a five line script. A pipeline that has to swallow scanned pages, shifting layouts, and someone else's exports every week is a maintenance commitment, and that is where a lot of teams quietly regret the DIY route.
Last updated July 2026.
Which Python library should I use to extract tables from a PDF?
Use pdfplumber for fine control on digital PDFs, camelot when the tables have visible ruling lines, and tabula-py if you already live in the Java ecosystem. All three read text based PDFs and return the table as data you can push into pandas. None of them read a scanned image without an OCR step bolted on first, which is the single most important thing to know before you pick one.
| Library | Best at | Needs | Reads scans? |
|---|---|---|---|
| pdfplumber | Precise control, digital PDFs, custom layouts | Pure Python, no external runtime | No, add OCR first |
| camelot | Tables with visible lines (lattice) or whitespace (stream) | Ghostscript for lattice mode | No, add OCR first |
| tabula-py | Familiar output, batch jobs | A Java runtime installed | No, add OCR first |
The pattern across all three is the same: they parse the text and coordinates the PDF already contains and reconstruct the grid from them. That is why they are fast and accurate on a clean export and why they return nothing useful on a scan, where the page is a picture and there is no text layer to parse.
How do you extract a table with pdfplumber?
Open the PDF, take a page, and call extract_tables, which returns each table as a list of rows. pdfplumber gives you the most control of the three because you can inspect words, lines, and bounding boxes and tune the table detection settings when a layout is awkward.
A minimal version looks like this:
| Step | Code |
|---|---|
| Import and open | import pdfplumber |
| Load the file | pdf = pdfplumber.open("statement.pdf") |
| Grab a page | page = pdf.pages[0] |
| Extract tables | tables = page.extract_tables() |
| Into pandas | df = pd.DataFrame(tables[0][1:], columns=tables[0][0]) |
When the automatic detection misfires, pdfplumber lets you switch the table strategy between lines and text, so a borderless table can be read by column position instead of ruling lines. That flexibility is its strength and also the reason it takes the most tinkering to get right on messy files.
What is the difference between camelot lattice and stream?
Lattice reads tables that have visible ruling lines by detecting those lines, and stream reads tables that rely on whitespace by inferring columns from the gaps between text. Camelot asks you to pick the flavor per document: lattice is accurate when the PDF draws borders around cells, and stream is the fallback for clean, well spaced tables with no lines. Lattice mode depends on Ghostscript being installed, which is a common first stumble.
Camelot returns each table as an object you can convert to a pandas DataFrame, and it reports an accuracy score per table so you can flag low confidence extractions programmatically. If your PDFs are visually ruled and consistent, camelot in lattice mode is often the least fiddly of the three. If they are inconsistent, you end up branching between the two modes, which adds the kind of logic that grows into a maintenance burden.
Why can't Python read my scanned PDF?
Because a scanned PDF has no text layer, it is an image of a page, and pdfplumber, camelot, and tabula-py all work by parsing text the PDF already contains. There is nothing for them to parse, so they return empty results. This trips up a lot of people, because the file opens and looks identical to a digital PDF on screen while being fundamentally different underneath.
To handle scans in Python you add an OCR stage first, typically running the pages through Tesseract with a wrapper like pytesseract or preprocessing the file with a tool such as ocrmypdf to add a text layer, then extracting as normal. That works, but it is a second dependency, a slower pipeline, and a new source of errors, since OCR mistakes such as a 0 read as O or a 1 read as l now flow into your data. If most of your documents are scans, the extra machinery is a real cost to weigh.
When is a no-code converter the better choice?
A script wins when the inputs are uniform, the volume is high, and you have engineers to keep it running. A no-code converter wins when the documents vary, when scans are common, or when the people who need the data are not developers. Writing the first extraction is easy; the expensive part is the long tail of edge cases, the OCR handling, and the maintenance when a source changes its layout and the script silently returns garbage.
Be honest about the total cost before committing to code. If you are extracting from a steady stream of clean, identical exports, the libraries above are excellent and free. If you are fielding mixed formats and scanned pages from clients, a tool built to handle both is usually cheaper than the engineering time. Our PDF table extractor and PDF to Excel converter cover the no-code path, including OCR for scanned pages and a CSV export for feeding other systems, and the broader trade-offs are laid out on how to extract tables from a PDF. When the data lives on a web page rather than in a PDF, a different tool is the right one, since scraping clean structured data straight off a web page is a separate problem from parsing a document.
The short version
- pdfplumber for control on digital PDFs, camelot for ruled or well spaced tables, tabula-py if you have Java.
- All three parse an existing text layer, so none read a scanned image without a separate OCR step.
- Adding OCR with Tesseract works but adds dependencies, latency, and a new class of character errors.
- Code pays off on uniform, high volume inputs with engineers to maintain it, and loses on mixed or scanned documents.
Python is a genuinely good answer when the conditions fit. The mistake is reaching for it by default and then quietly funding a maintenance project every time a vendor changes an export.