fitz is the import name for PyMuPDF — you install PyMuPDF and import fitz — and it is the fastest way to pull text out of a PDF in Python.
The name mismatch is a historical artefact that generates a steady stream of confused questions, and it has been partially resolved: newer versions let you import pymupdf instead, while keeping fitz working. The library itself is excellent, and it is licensed in a way that catches commercial projects off guard.
Table of contents
- Install and the naming situation
- Extracting text
- When the text comes back empty
- Tables and images
- Memory, and why batch jobs fall over
- Running it as a service
- How this fits the rest of the stack
- FAQ
Install and the naming situation
pip install PyMuPDF
The package is PyMuPDF. The module is fitz, named after MuPDF’s original internal codename.
import fitz # works everywhere
import pymupdf # works on 1.24 and later, same library
Use pymupdf in new code if your version supports it. Keep fitz for anything that has to run on older installations — the vast majority of examples online use it, so it is not going anywhere.
One thing to check before you build on this: PyMuPDF is AGPL-licensed. For internal tools and open source, fine. If you are embedding it in a product you distribute, or serving it over a network in a commercial application, the AGPL’s network clause applies and you need either a commercial licence from Artifex or a different library. This surprises people late, which is the worst time to find out.
If that rules it out, pypdf is MIT-licensed and adequate for basic text extraction, just slower and less capable on complex layouts.
Extracting text
import fitz
with fitz.open("report.pdf") as doc:
print(doc.page_count)
print(doc.metadata["title"])
for page in doc:
print(page.get_text())
Use the context manager. A Document holds an open file handle and native memory; a batch job that opens documents in a loop without closing them will exhaust file descriptors, and the error surfaces somewhere unrelated.
get_text() takes a mode argument, and picking the right one saves a lot of post-processing:
"text"— plain text in reading order. The default and usually what you want."blocks"— a list of tuples with coordinates. Use this when position matters, such as separating a header from body copy."dict"— full structure: blocks, lines, spans, with font, size, and colour. This is how you find headings without guessing."words"— one entry per word with a bounding box. Good for locating a specific term on the page."html"— layout-preserving HTML.
# find every span larger than 14pt -- probably headings
with fitz.open("report.pdf") as doc:
for page in doc:
for block in page.get_text("dict")["blocks"]:
for line in block.get("lines", []):
for span in line["spans"]:
if span["size"] > 14:
print(round(span["size"], 1), span["text"])
That span-size approach is far more reliable than pattern-matching on text, because a document’s own typography already encodes its structure.
When the text comes back empty
A scanned PDF is images of pages. There is no text layer, so get_text() correctly returns an empty string. The document is not corrupt and the library is not failing.
with fitz.open("scan.pdf") as doc:
text = "".join(p.get_text() for p in doc)
if not text.strip():
print("no text layer -- this needs OCR")
That check is worth having in any pipeline that ingests documents from users, because the failure is otherwise silent — you index nothing and nobody notices until someone searches for a document that is definitely in the system.
For OCR, render the page to an image and hand it to a proper OCR engine:
with fitz.open("scan.pdf") as doc:
page = doc[0]
pix = page.get_pixmap(dpi=300) # 300 dpi is the usual OCR floor
pix.save("page-0.png")
PyMuPDF has built-in Tesseract integration through page.get_textpage_ocr() if Tesseract is installed on the system, which saves the round trip through disk.
Tables and images
Table extraction landed in 1.23 and handles ruled tables well:
with fitz.open("invoice.pdf") as doc:
for page in doc:
for table in page.find_tables():
rows = table.extract()
for row in rows:
print(row)
It works on tables with visible borders. Tables laid out purely with whitespace are a genuinely hard problem and no library solves them reliably — if that is your input, "words" mode plus your own column detection based on x-coordinates will get further than fighting the table finder.
Extracting embedded images:
with fitz.open("doc.pdf") as doc:
for page_num, page in enumerate(doc):
for i, img in enumerate(page.get_images(full=True)):
xref = img[0]
base = doc.extract_image(xref)
with open(f"p{page_num}-{i}.{base['ext']}", "wb") as fh:
fh.write(base["image"])
This pulls out the original embedded image at its stored resolution, rather than a re-render of the page. Note that the same image referenced on multiple pages appears once per page — deduplicate on xref if that matters.
Memory, and why batch jobs fall over
PyMuPDF is a binding over a C library, and the memory it allocates is not Python heap. The garbage collector cannot see most of it, so a process that looks fine by sys.getsizeof can be using several gigabytes.
The two rules that prevent this:
# 1. always close -- context manager or explicit
with fitz.open(path) as doc:
...
# 2. free pixmaps, which are the real memory hogs
pix = page.get_pixmap(dpi=300)
pix.save(out)
pix = None # release immediately
A single 300 dpi pixmap of an A4 page is roughly 25 megabytes uncompressed. Rendering a 200-page document in a loop without releasing them is five gigabytes of native memory, and the process gets killed by the OOM reaper rather than raising a Python exception you could catch.
For large batches, process each document in a subprocess. It sounds heavy-handed and it is completely reliable — when the worker exits, the operating system reclaims everything regardless of what the library did internally.
from concurrent.futures import ProcessPoolExecutor
with ProcessPoolExecutor(max_workers=4) as pool:
results = list(pool.map(extract_one, pdf_paths))
Running it as a service
PDF processing usually ends up behind an upload endpoint, and that shape has some sharp edges worth naming.
It is CPU-bound and slow enough to matter. A 200-page render will not finish inside a web request timeout, so the work belongs in a background job with the request returning a job id immediately. Doing it inline is how an upload feature takes down the API for everyone.
It is also a parser processing untrusted input. Malformed PDFs are a real attack surface — keep the library current, cap the file size before you open it, and cap the page count before you render anything.
That makes it a natural fit for a separate worker service: a web service that accepts the upload and writes to storage, a worker that does the extraction, and a database holding the results. On RunxBuild that is a service, a worker, object storage, and a managed database, with credentials injected as environment variables and runtime logs per deploy — so when a specific document breaks extraction you can find the traceback rather than guessing which upload it was.
How this fits the rest of the stack
Document processing is memory-hungry and bursty, which is exactly the kind of workload that makes a bill jump without warning. The RunxBuild hosting calculator puts the runtime, worker, storage, and database line items on one page so you can model the whole shape before committing.
Useful related references:
- Python Not Equal: != vs is not, and Why the Difference Bites
- Python Integer Division: Why // Floors and Why -7 // 2 Is -4
- Python for Websites: Where It Fits and Where It Does Not
- Python services on RunxBuild
FAQ
Why does PyMuPDF import as fitz?
fitz was MuPDF’s original internal codename and the binding kept it. You install PyMuPDF and import fitz. Version 1.24 and later also allow import pymupdf, which is the better choice in new code while fitz continues to work everywhere.
Is PyMuPDF free to use commercially?
It is AGPL-licensed, so the network clause applies if you serve it in a commercial application or distribute it in a product. That requires a commercial licence from Artifex. For internal tools and open source it is fine. pypdf is MIT-licensed if the AGPL rules it out.
Why does get_text return an empty string?
The PDF is almost certainly a scan — images of pages with no text layer. The library is behaving correctly. Check for empty output explicitly and route those documents to OCR, either by rendering pages with get_pixmap or using the built-in Tesseract integration.
How do I stop PyMuPDF from using so much memory?
Close documents with a context manager and release pixmaps by setting them to None as soon as they are saved. A single 300 dpi A4 pixmap is around 25MB of native memory the garbage collector cannot see. For large batches, process each document in a subprocess so the OS reclaims everything on exit.
Can PyMuPDF extract tables?
Yes, page.find_tables() from version 1.23 onward handles tables with visible borders well. Tables laid out with whitespace alone are unreliable in every library — for those, use words mode and detect columns from x-coordinates yourself.