PDF table extraction often looks right on screen and still breaks downstream: a merged cell shifts a column, a header is read as data, or a page break drops a row. This tutorial shows a small structure check you can run right after extraction with pdfplumber.
1. Extract the tables
import pdfplumber
def extract_tables(path):
tables = []
with pdfplumber.open(path) as pdf:
for page_no, page in enumerate(pdf.pages, start=1):
for table in page.extract_tables():
tables.append({"page": page_no, "rows": table})
return tables
2. Check the structure
def check_table(rows, expected_header=None):
problems = []
if not rows:
return ["empty table"]
width = len(rows[0])
for i, row in enumerate(rows):
if len(row) != width:
problems.append(f"row {i}: {len(row)} cells, expected {width}")
empty = sum(1 for c in row if c is None or str(c).strip() == "")
if empty == len(row):
problems.append(f"row {i}: fully empty")
if expected_header is not None and [str(c).strip() for c in rows[0]] != expected_header:
problems.append("header does not match expected columns")
return problems
3. Run it
for t in extract_tables("report.pdf"):
issues = check_table(t["rows"], expected_header=["Item", "Qty", "Price"])
status = "OK" if not issues else "; ".join(issues)
print(f"page {t['page']}: {len(t['rows'])} rows -> {status}")
What this catches: ragged rows (a column shifted by a merged cell), fully empty rows from page breaks, and header drift. What it does not catch: a value that is wrong but well-formed. For that, compare totals against the source.
Next steps
If your tables have merged headers, extract with explicit table_settings in pdfplumber (see the pdfplumber README) and run the same check per header block.
This article was written with AI assistance and checked by the author.
How do you validate extracted PDF tables today: totals check, schema, or by eye?
Top comments (0)