pymupdf
PyMuPDF is a Python binding over MuPDF, the C rendering engine from Artifex. The C layer is why it is fast: text extraction, page rasterization, and document surgery all happen in compiled code with the whole engine shipped inside the wheel, so there are no other Python dependencies. One library covers reading and writing. You can pull plain text or a structured dict with per-span font, size, color, and bounding box; detect and export tables; render any page to a PNG at any DPI; add and apply real redactions that delete the underlying content; fill AcroForm fields; merge, split, rotate, and encrypt documents. It opens more than PDF too: XPS, EPUB, CBZ, MOBI, FB2, SVG, plain images, and Markdown. Version 1.28.0 wraps MuPDF 1.29.0.
On speed and breadth PyMuPDF has no real competition in Python, and if you are already AGPL or paying Artifex it should be the default. The licence is the decision, not the code: for a closed-source product, settle that question before the first import, because migrating away later means rewriting every coordinate.
Use it if
- You are processing documents at volume and per-page cost matters, such as an ingestion pipeline turning thousands of PDFs into text or page images
- Plain text is not enough and you need layout metadata: font name, size, color, and the bounding box of every span, which get_text('dict') gives you directly
- You need to render pages to images, whether for thumbnails, a preview UI, or feeding page bitmaps to a vision model at a chosen DPI
- You need real redaction. apply_redactions removes the text and images under the rectangle instead of drawing a black box over content that a copy-paste would still reveal
- One library needs to both read and write: extract, annotate, fill forms, merge, split, and save encrypted, without stitching together three packages that disagree about coordinates
- You cannot comply with AGPL-3.0 and will not buy a license. The package is dual licensed as AGPL or an Artifex commercial license, so shipping it inside a closed-source product or SaaS means either releasing your source under AGPL or paying. pypdf and pdfplumber are permissive and this question never comes up
- Install size is constrained. Wheels run 17 to 25 MB and unpack to roughly 64 MB of site-packages because the MuPDF engine is bundled, which is real weight for a Lambda layer or a slim container image
- You need Office formats. DOCX, XLSX, PPTX, HWP, and HWPX only work through the paid PyMuPDF Pro add-on, and without a key you get the first 3 pages of any document plus a time-limited evaluation period
- You expect OCR to be included. Scanned pages need Tesseract installed separately at the system level and on PATH; PyMuPDF only calls out to it
- All you do is split, merge, and rotate PDFs. pypdf is pure Python, permissively licensed, installs in a second, and does that job fine
- You want a small API. This is a wide surface with MuPDF vocabulary (Pixmap, TextPage, DisplayList, Xref, Story) and a top-left origin coordinate system that is the opposite of the PDF specification's bottom-left, so geometry code ported from other libraries lands upside down
Setup reality
pip install pymupdf pulls a single wheel with no Python dependencies, but it is a 17 to 25 MB download that expands to about 64 MB on disk because the MuPDF C library rides along. The wheels are abi3 for CPython 3.10 and up and cover manylinux_2_28 on x86_64 and aarch64, musllinux_1_2 on x86_64 only, macOS on both architectures, Windows 32 and 64 bit, plus a WebAssembly build. Alpine on ARM falls off that list and compiles the 84 MB source distribution, which needs a full C and C++ toolchain and takes a long while. The import name is pymupdf; the historical import fitz still resolves to the same module but is the old spelling and new code should not use it. The part people skip is licensing: AGPL-3.0 or a paid Artifex license, decided before the code ships, not after. Incremental saves have their own trap and need encryption=pymupdf.PDF_ENCRYPT_KEEP or MuPDF refuses with 'Can't do incremental writes when changing encryption'.
Patterns
Pull text out of every pageextract-plain-text
import pymupdf
doc = pymupdf.open('document.pdf')
for page in doc:
print(page.get_text())
# reading order can be wrong on multi-column pages
print(doc[0].get_text(sort=True))
doc.close()Default extraction follows the order text was written into the file, which is not reading order on newspapers or two-column papers. sort=True reorders by vertical then horizontal position and fixes most of those cases.
Get font, size, and position for every spanextract-with-layout
page = doc[0]
for block in page.get_text('dict')['blocks']:
if block['type'] != 0: # 0 is text, 1 is image
continue
for line in block['lines']:
for span in line['spans']:
print(span['text'], span['font'], round(span['size'], 1), span['bbox'])This is how you detect headings without a heuristic on line length: compare span sizes against the document's body size. Image blocks have type 1 and no lines key, so skipping them is required or you get a KeyError.
Rasterize a page at a chosen resolutionrender-page-to-image
page = doc[0]
pix = page.get_pixmap(dpi=150)
pix.save('page_0.png')
img_bytes = pix.tobytes('png') # for an in-memory pipeline
print(pix.width, pix.height, pix.n)Memory grows with the square of the DPI, so 600 dpi on an A4 page is roughly 35 megapixels and can be hundreds of megabytes across a worker pool. 150 dpi is enough for OCR; 300 only when the source is genuinely small print.
Find text and annotate the hitssearch-and-highlight
page = doc[0]
hits = page.search_for('confidential')
for rect in hits:
page.add_highlight_annot(rect)
doc.save('highlighted.pdf')search_for is case-insensitive and returns Rect objects in the top-left origin system PyMuPDF uses for pages, not the PDF specification's bottom-left. It also cannot match across a line break, so hyphenated or wrapped phrases come back empty.
Permanently remove text from a pageredact-content
page = doc[0]
for rect in page.search_for('social security'):
page.add_redact_annot(rect, fill=(0, 0, 0))
page.apply_redactions()
doc.save('redacted.pdf')apply_redactions actually deletes the underlying text and image content inside the rectangle, unlike drawing a black rectangle which leaves the text selectable. Nothing happens until you call it, and it must run before save.
Find tables and export themextract-tables
page = doc[0]
tabs = page.find_tables()
for table in tabs.tables:
rows = table.extract() # list of lists of strings
print(table.to_markdown())
df = table.to_pandas() # needs pandas installedDetection leans on ruling lines, so borderless tables often come back as zero tables or as one merged blob. Recent versions also print a hint suggesting the separate pymupdf_layout package for better page analysis. to_pandas raises ImportError if pandas is absent.
Combine documents and cut pages outmerge-and-split
merged = pymupdf.open()
for path in ['a.pdf', 'b.pdf']:
with pymupdf.open(path) as src:
merged.insert_pdf(src)
merged.save('merged.pdf', garbage=4, deflate=True)
with pymupdf.open('big.pdf') as src:
out = pymupdf.open()
out.insert_pdf(src, from_page=0, to_page=9)
out.save('first10.pdf')pymupdf.open() with no argument creates an empty PDF to build into. garbage=4 plus deflate=True is what actually shrinks the output; without them a merge of two small files can be larger than the inputs combined.
Work on a document you never wrote to diskopen-from-bytes
data = await response.read()
doc = pymupdf.open(stream=data, filetype='pdf')
text = doc[0].get_text()
out_bytes = doc.tobytes(garbage=4, deflate=True)
doc.close()filetype is required with stream, because there is no filename to sniff from. tobytes gives you the saved document back as bytes so nothing touches the filesystem, which matters in read-only containers.
Append changes to an existing file in placeincremental-save
doc = pymupdf.open('report.pdf')
doc[0].insert_text((72, 200), 'reviewed')
doc.save('report.pdf', incremental=True, encryption=pymupdf.PDF_ENCRYPT_KEEP)
doc.close()Without encryption=pymupdf.PDF_ENCRYPT_KEEP this raises FzErrorArgument: Can't do incremental writes when changing encryption. Incremental saves are fast and preserve signatures, but they only append, so the file grows with every edit.
Save with a password and permission flagsencrypt-document
perm = int(
pymupdf.PDF_PERM_ACCESSIBILITY
| pymupdf.PDF_PERM_PRINT
)
doc.save(
'protected.pdf',
encryption=pymupdf.PDF_ENCRYPT_AES_256,
owner_pw='owner-secret',
user_pw='user-secret',
permissions=perm,
)Opening an encrypted file gives you a Document with needs_pass set and no readable pages until authenticate(password) returns non-zero. Permission flags are advisory and enforced only by well-behaved viewers.
Pull the original images out of a PDFextract-embedded-images
for page_index, page in enumerate(doc):
for img_index, img in enumerate(page.get_images(full=True)):
xref = img[0]
pix = pymupdf.Pixmap(doc, xref)
if pix.n > 4: # CMYK
pix = pymupdf.Pixmap(pymupdf.csRGB, pix)
pix.save(f'p{page_index}_i{img_index}.png')get_images returns the same xref once per page that references it, so a repeated logo gets extracted many times; deduplicate on xref. The CMYK conversion is required because Pixmap.save cannot write a 5-component image to PNG.
Run OCR on a page with no text layerocr-scanned-page
page = doc[0]
if not page.get_text().strip():
tp = page.get_textpage_ocr(language='eng', dpi=300, full=True)
text = page.get_text(textpage=tp)
print(text)Tesseract has to be installed at the OS level and reachable, and TESSDATA_PREFIX must point at the language data or this raises rather than falling back. OCR is orders of magnitude slower than native extraction, so gate it on an empty text layer as shown.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| pypdf | PyPI | You only need to split, merge, rotate, encrypt, or pull rough text, and you need a permissive license |
| pdfplumber | PyPI | Table and layout extraction accuracy matters more than speed, and you want per-character positional data under an MIT license |
| pikepdf | PyPI | You are editing PDF structure, objects, and streams rather than extracting content, and you want qpdf semantics with a permissive license |
| pymupdf4llm | PyPI | You want chunked Markdown output for a RAG pipeline instead of writing the layout-to-Markdown conversion yourself |