mrkeyoor.com_
Thu 06 Aug 10:11 UTC
PyPIDataupdated 06 Aug 2026

pdfplumber

pdfplumber reads a machine-generated PDF and hands you its raw geometry: every character with its font, size, and bounding box, plus every line, rectangle, curve, image, and annotation on the page. On top of that primitive layer it adds the two things people actually come for. First, a text extractor that can approximate the visual layout instead of flattening everything into one string. Second, a table finder that rebuilds a cell grid from the page's ruling lines, or from word alignment when the table has no lines at all. It can also render any page to a PNG and draw the detected edges and cells on top, which turns table debugging from guesswork into looking at a picture. It is built on pdfminer.six and inherits that library's parsing accuracy along with its speed.

Verdict

The right first choice for getting structured data out of digital PDFs, and the visual debugger alone saves the hours that otherwise go into guessing why a table came out wrong. Look elsewhere if your documents are scans or if throughput matters more than fidelity.

API stability4/5The core surface of open, pages, crop, extract_text, extract_words, and extract_table has held for years and the changelog flags breaking changes clearly. It is still 0.x after 76 releases, and object attributes can disappear in a patch release when the pinned pdfminer.six changes, which is what happened to stroking_pattern in 0.11.7.
Docs5/5A single long README documents every object property, every table setting with its default, and every method argument, then adds a section stating plainly what the library does not do and how it compares to pymupdf, camelot, tabula, and pypdf. Example notebooks work through real documents rather than toy files.
Maintenance4/5jsvine has maintained it since 2016 and the repository had commits on the day of this review, with 0.11.10 released on 2026-06-15. Cadence is a few releases a year, mostly pdfminer.six bumps rather than features, and 76 issues are open on the tracker with support questions routed to Discussions instead.
Ecosystem5/5Around 14.2M downloads a week, and it is the default PDF reader inside document pipelines and retrieval loaders. Almost every guide to parsing PDFs in Python has a pdfplumber path, so answers to specific extraction problems are easy to find.

Use it if

  • You need coordinates and not just text: pulling the value that sits to the right of a label, or reading a fixed region of a form, is a crop plus an extract_text call
  • You extract tables from digital PDFs that have visible ruling lines and you want a row-by-cell grid rather than a text blob
  • Your tables have no ruling lines and you are willing to tune detection yourself, using the text strategy, explicit line lists, and the snap, join, and intersection tolerances
  • You want to see why extraction went wrong: to_image().debug_tablefinder() draws the edges, intersections, and cells the algorithm actually found
  • You need per-character font name and size to detect headings, bold runs, footnote markers, or where one visual column ends
Skip it if

Setup reality

pip install pdfplumber pulls pdfminer.six at one exact pinned build, Pillow, and pypdfium2, which ships a prebuilt PDFium binary inside its wheel. There is no compiler step on mainstream platforms, and there is no ImageMagick or Ghostscript to install; tutorials that tell you to install those predate 0.10.0, where Wand was replaced by pypdfium2 for page rendering. The friction shows up after the install. That exact pdfminer.six pin makes pip give up if anything else in the environment wants a different version. Visual debugging is built for notebooks, so im.show() on a headless box tries to launch a desktop image viewer and accomplishes nothing; save to a file instead. Metadata that fails to parse is only a warning unless you pass strict_metadata=True. Coordinates are in PDF points measured from the top of the page, which is upside down relative to the PDF specification's own bottom-left origin, and getting that backwards is the most common reason a crop returns nothing.

Patterns

Open a PDF and read a pageopen-and-extract-text

import pdfplumber

with pdfplumber.open("report.pdf") as pdf:
    print(len(pdf.pages), "pages")
    page = pdf.pages[0]
    print(page.extract_text())

open accepts a path, bytes, or any file-like object, plus password= for encrypted files. Use it as a context manager; otherwise the file handle stays open and per-page caches are never released.

Keep visual layout, or fix run-together wordslayout-and-tolerances

# rough visual layout: spaces padded to approximate x position
print(page.extract_text(layout=True))

# words running together or splitting apart? tune the tolerance
print(page.extract_text(x_tolerance=1.5))
print(page.extract_text(x_tolerance_ratio=0.15))

layout=True is still labelled experimental and pads output using x_density and y_density, so it is good for eyeballing structure and bad as a parsing target. When spacing is wrong, x_tolerance is the knob; x_tolerance_ratio scales it with font size, which helps documents that mix 8pt and 20pt text.

Pull tables off a pageextract-table

largest = page.extract_table()        # row -> cell, or None
if largest:
    for row in largest:
        print(row)

for table in page.extract_tables():   # table -> row -> cell
    print(len(table), "rows")

Cells come back as strings or None, where None means the grid had a cell there but no text in it. extract_table returns None outright when nothing was found, so guard it before iterating.

Extract a table drawn with whitespaceborderless-table

settings = {
    "vertical_strategy": "text",
    "horizontal_strategy": "text",
    "min_words_vertical": 3,
    "text_x_tolerance": 2,
}
table = page.extract_table(settings)

The default strategy is "lines" and finds nothing on a table with no ruling lines. "text" infers column edges from word alignment, which means it also invents columns out of ordinary paragraphs, so crop to the table region first. "lines_strict" ignores rectangle sides, useful when a shaded header row is a filled rect rather than four lines.

Read one region of the pagecrop-region

# (x0, top, x1, bottom) in points, measured from the TOP of the page
header = page.crop((0, 0, page.width, 120))
print(header.extract_text())

# keep only objects that fall entirely inside
body = page.within_bbox((40, 120, page.width - 40, page.height - 60))

crop keeps objects that only partly overlap the box and slices them; within_bbox keeps only objects entirely inside; outside_bbox is the inverse. The vertical axis runs downward from the top, the opposite of the PDF specification's bottom-left origin, and mixing that up is the usual reason a crop comes back empty.

Get words with positions and font datawords-with-coordinates

words = page.extract_words(extra_attrs=["fontname", "size"])

for w in words:
    if w["size"] > 14:
        print(w["text"], w["x0"], w["top"], w["fontname"])

extra_attrs both filters and reports: characters only join into one word when they share every listed attribute, so adding "size" splits a token wherever the font size changes. Drop the attrs if you are getting fragments where you expected whole words.

Find a label and read what sits beside itvalue-next-to-label

hits = page.search(r"Invoice\s+No", regex=True, case=False)

if hits:
    box = hits[0]
    right = page.within_bbox(
        (box["x1"], box["top"] - 2, page.width, box["bottom"] + 2)
    )
    print(right.extract_text())

search is an experimental feature and returns dicts carrying x0, x1, top, bottom, the regex groups, and the matched chars. Zero-width and whitespace-only matches are discarded, so a pattern that can match nothing hands you back nothing.

Look at what the table finder sawvisual-debug

im = page.to_image(resolution=150)
im.debug_tablefinder(settings)
im.save("page1-tables.png")

im2 = page.to_image()
im2.draw_rects(page.extract_words())
im2.save("page1-words.png")

debug_tablefinder draws detected edges in red, intersections as circles, and tables in light blue, which is the fastest way to tell whether your strategy found the grid or hallucinated one. im.show() opens a desktop viewer and does nothing over SSH, so save a file instead.

Loop a large document without leaking memorylarge-pdf-memory

with pdfplumber.open("10k-pages.pdf") as pdf:
    for page in pdf.pages:
        handle(page.extract_text())
        page.close()   # release this page's cached layout

Page objects cache parsed layout and object lists and never drop them on their own, so a long loop climbs in memory until the process is killed. close() flushes that cache and the page reparses on demand if you touch it again.

Detect a scanned page before you trust the outputdetect-scanned-page

text = page.extract_text() or ""

if not page.chars and page.images:
    raise RuntimeError("page looks scanned: OCR it first")

There is no OCR in this library and no signal when a page is just a photograph of text. Checking for zero chars is the standard guard before routing the file to tesseract, ocrmypdf, or a vision model.

Fix doubled letters from fake-bold textdedupe-chars

clean = page.dedupe_chars(tolerance=1)
print(clean.extract_text())

Some generators fake bold by drawing the same glyph twice a fraction of a point apart, which makes extract_text return things like "ttoottaall". dedupe_chars drops the near-duplicates first, matching on text, position, fontname, and size.

Inspect an unfamiliar PDF from the shellcli-inspect

pdfplumber report.pdf --format csv --pages 1 3-5 > objects.csv

pdfplumber report.pdf --format json --types char rect line > objects.json

pdfplumber report.pdf --format text

The CLI dumps every object it finds, which is the quickest way to see what a file you have never opened is actually made of. --pages is 1-indexed and space delimited, unlike the 0-indexed pdf.pages list you use from Python.

Alternatives

PackageRegistryPick it when
pymupdfPyPIYou are processing thousands of pages and raw speed matters more than shape-level detail, and the AGPL terms or a commercial license work for you
pypdfPyPIYou need to merge, split, rotate, encrypt, or fill PDFs rather than read data out of them
camelot-pyPyPITable extraction is the entire job and pdfplumber's line-based finder keeps missing your layouts
pdfminer.sixPyPIYou only want text and layout boxes and would rather not carry the table finder, the renderer, and Pillow