pypdf
pypdf is a pure-Python library for taking apart and reassembling PDF files that already exist. Two classes cover almost everything: PdfReader opens a document and gives you pages, metadata, form fields, embedded images, and best-effort text; PdfWriter collects pages from one or more readers and writes a new file. With those you split, merge, rotate, crop, encrypt, decrypt, fill AcroForm fields, edit metadata, and add outline entries. It is the continuation of PyPDF2 under the py-pdf organization, so the project name changed but the lineage is the same, which is why so much old sample code no longer runs.
The default pick for page-level PDF surgery in Python: pure, permissively licensed, actively released, and easy to reason about. It is not a text extraction engine or a renderer, so pair it with pdfplumber or PyMuPDF the moment you care about layout or pixels.
Use it if
- You need to split, merge, reorder, rotate, or crop pages of PDFs that a user or another system produced
- You want a pure-Python dependency with no compiler, no system libraries, and no restrictive license, which matters in locked-down build environments and AWS Lambda layers
- You need to read or write document metadata, add outline (bookmark) entries, or fill AcroForm fields programmatically
- You want quick text extraction for search indexing or a document pipeline where perfect fidelity is not required
- You need to render a page to an image: pypdf has no rasterizer at all, so thumbnails and page previews require PyMuPDF or a poppler binding
- Your PDFs are scans: there is no OCR, and a scanned page returns an empty string with no warning that the page was an image
- You care about reading order: extraction follows the content stream, so multi-column pages and tables interleave. extraction_mode='layout' helps, and pdfplumber with per-word coordinates helps more
- You are creating documents from scratch: there is no text layout engine, no flowables, no charting. reportlab does that job and pypdf edits the result
- You are processing large or image-heavy files at volume: pure Python costs you real time next to the C-backed options
- You cannot pin dependencies: majors land about yearly and prune deprecated names (5.0.0 removed PdfMerger and AnnotationBuilder, 6.0.0 dropped Python 3.8), with 14 minor releases between 6.0.0 and 6.14.2
Setup reality
pip install pypdf takes seconds, needs no compiler, and pulls in nothing at runtime except typing_extensions on Python 3.9 and 3.10. The friction is the extras: AES encryption needs pypdf[crypto], reading embedded images needs pypdf[image] for Pillow, embedding TrueType fonts needs pypdf[fonts], and pypdf[full] takes all three. You discover which one you needed by hitting an ImportError mid-run rather than at install time. The bigger time sink is stale sample code. pypdf absorbed PyPDF2, and most Stack Overflow answers still use PdfFileReader, PdfFileWriter, getPage, and PdfMerger, none of which exist anymore, so anything written before pypdf 3 needs translating before it will even import.
Patterns
Read text from every pageextract-text
from pypdf import PdfReader
reader = PdfReader("example.pdf")
print(len(reader.pages))
for page in reader.pages:
text = page.extract_text()
print(text)An empty string usually means the page is a scanned image, not that extraction failed. pypdf has no OCR, so check for it and route those pages elsewhere.
Keep visual column layout when extractingextract-text-layout
page = reader.pages[0]
text = page.extract_text(extraction_mode="layout")
# tune spacing for dense pages
text = page.extract_text(
extraction_mode="layout",
layout_mode_space_vertically=False,
)Default mode follows the content stream, which interleaves multi-column pages. Layout mode reconstructs spatial position and is slower, but it is the difference between usable and scrambled output on invoices and reports.
Merge whole files and page rangesmerge-pdfs
from pypdf import PdfWriter
writer = PdfWriter()
writer.append("cover.pdf")
writer.append("body.pdf", pages=(0, 10))
writer.append("appendix.pdf")
writer.write("combined.pdf")
writer.close()PdfMerger was removed in pypdf 5.0.0; PdfWriter.append replaces it. The pages tuple is a (start, stop) slice, so (0, 10) is the first ten pages.
Write each page to its own filesplit-pages
reader = PdfReader("report.pdf")
for i, page in enumerate(reader.pages):
writer = PdfWriter()
writer.add_page(page)
with open(f"page_{i + 1}.pdf", "wb") as out:
writer.write(out)Each output still carries the source document's shared resources, so splitting a 200-page file rarely gives you files one two-hundredth the size.
Rotate a page and trim its boxrotate-and-crop
reader = PdfReader("scan.pdf")
writer = PdfWriter()
page = reader.pages[0]
page.rotate(90)
page.mediabox.lower_left = (36, 36)
page.mediabox.upper_right = (576, 756)
writer.add_page(page)
writer.write("trimmed.pdf")rotate() only accepts multiples of 90 and mutates the page in place. Box coordinates are PDF points (72 per inch) measured from the bottom-left corner, not pixels from the top.
Inspect document metadataread-metadata
reader = PdfReader("example.pdf")
meta = reader.metadata
print(meta.title)
print(meta.author)
print(meta.creator)
print(meta.creation_date)reader.metadata is None for documents with no info dictionary, so guard the attribute access. Every field is optional and producers fill them inconsistently.
Set metadata on the output filewrite-metadata
writer = PdfWriter(clone_from="example.pdf")
writer.add_metadata({
"/Title": "Q3 Report",
"/Author": "Finance",
"/Producer": "pypdf",
})
writer.write("tagged.pdf")Keys must include the leading slash. clone_from copies the whole source document, which preserves outlines and form structure that a page-by-page copy would drop.
Password-protect the outputencrypt-pdf
writer = PdfWriter(clone_from="private.pdf")
writer.encrypt(
user_password="open-me",
owner_password="full-access",
algorithm="AES-256",
)
writer.write("protected.pdf")AES needs the crypto extra (pip install pypdf[crypto]) or you get an ImportError at the encrypt() call. Leaving algorithm unset falls back to weak RC4, so always name it.
Open an encrypted documentdecrypt-pdf
reader = PdfReader("protected.pdf")
if reader.is_encrypted:
result = reader.decrypt("open-me")
if result == 0:
raise ValueError("wrong password")
text = reader.pages[0].extract_text()decrypt returns a PasswordType enum whose zero value means failure; it does not raise. Touching reader.pages before decrypting raises a FileNotDecryptedError.
Pull embedded images off a pageextract-images
reader = PdfReader("catalog.pdf")
page = reader.pages[0]
for image in page.images:
with open(image.name, "wb") as fh:
fh.write(image.data)
# image.image is a Pillow object when you need to resize or convertRequires the image extra (pip install pypdf[image]) for Pillow. This returns images embedded as objects, not a rendering of the page, so vector artwork and text never appear here.
Fill AcroForm fieldsfill-form-fields
reader = PdfReader("form.pdf")
print(reader.get_fields().keys())
writer = PdfWriter(clone_from=reader)
writer.update_page_form_field_values(
writer.pages[0],
{"full_name": "Ada Lovelace", "agree": "/Yes"},
auto_regenerate=False,
)
writer.write("filled.pdf")Checkboxes take the export value with a leading slash, not True. auto_regenerate=False stops pypdf asking the viewer to rebuild appearances, which is what you want when the values should show up in every reader.
Add outline (bookmark) entriesadd-outline
writer = PdfWriter(clone_from="book.pdf")
parent = writer.add_outline_item("Part One", 0)
writer.add_outline_item("Chapter 1", 1, parent=parent)
writer.add_outline_item("Chapter 2", 12, parent=parent)
writer.write("book-with-toc.pdf")Page numbers are zero-based indexes into writer.pages, not the printed page labels. Building pages with add_page instead of clone_from drops the source document's existing outline entirely.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| pdfplumber | PyPI | You need word and character coordinates, table extraction, or accurate reading order |
| pymupdf | PyPI | Speed matters or you need to render pages to images; check that the AGPL or a commercial license fits |
| pikepdf | PyPI | Lossless structural surgery on the PDF object model, backed by qpdf |
| reportlab | PyPI | Generating new PDFs with real layout, tables, and charts rather than editing existing ones |