PyPDF2
PyPDF2 is a pure-Python library for working with existing PDF files: reading pages, pulling out text and metadata, splitting and merging documents, rotating and scaling and stamping pages, editing form field values, and adding or removing passwords. It has no native dependencies, so it installs anywhere Python does. It is also over: the project renamed itself back to pypdf, 3.0.1 from December 2022 is the last PyPDF2 release, and importing the package emits a DeprecationWarning telling you to move. The repository link still resolves, but it now points at the pypdf project that replaced it.
A capable pure-Python PDF toolkit that stopped at 3.0.1 in December 2022 and now warns you about itself on import. Keep it only to service existing code, and budget an afternoon to rename the import to pypdf, which is the same project still being maintained.
Use it if
- You are maintaining code that already imports PyPDF2 and need to know exactly what 3.0.1 can do before planning the move to pypdf
- You need page-level manipulation of existing PDFs, splitting, merging, rotating, cropping, stamping, with no compiled dependency and no system libraries
- You are on a locked-down environment where a pure-Python wheel is the only thing you can install and Python 3.6 or 3.7 is still in play
- You want to read metadata, outlines or AcroForm field values out of a document without rendering anything
- You are writing anything new, because pypdf is the same project under its original name with three further years of fixes, and the migration is largely an import rename
- You need to encrypt output with AES, since the encrypt method in 3.0.1 only writes RC4 at 40 or 128 bits, and the crypto extra covers reading AES documents rather than writing them
- You need text extraction to be reliable on complicated layouts, because this is a pure content-stream walk with no layout model, so columns, tables and rotated text come back scrambled
- You want images, rendering or page rasterisation, which the library does not do; it can list embedded images but cannot draw a page
- You want bug fixes: 3.0.1 shipped on 2022-12-31 and is final by the maintainers' own statement, so anything found since is fixed only in pypdf
- You are creating documents from scratch, which is a layout problem this library was never built for
Setup reality
pip install PyPDF2 is quick and pure Python, with typing_extensions pulled in only below 3.10 and dataclasses only below 3.7. The extras matter more than people expect: install PyPDF2[crypto] to get PyCryptodome if you must open AES-encrypted documents, and PyPDF2[image] to get Pillow for the images property. Then the first thing you meet is a DeprecationWarning at import time, emitted from the package's own __init__, saying PyPDF2 is deprecated and to move to pypdf. That is not a lint warning, it is the maintainers' position, and if your test suite turns warnings into errors your build fails on the import line. Naming is the next hurdle. Version 3.0 completed the rename from the old camelCase surface, so PdfReader, PdfWriter and PdfMerger are the current names while PdfFileReader, PdfFileWriter and PdfFileMerger are still exported and still warn, which means old tutorials and current code disagree on every example. Inside the classes the same split repeats: add_page next to addPage, extract_text next to extractText, get_page next to getPage. Reading defaults to strict=False, so a malformed file is repaired silently where an older version would have raised; pass strict=True while you are debugging and you will see problems the default hides. Encryption is the part to check before shipping. The encrypt method writes standard security handler revisions 2 and 3, which is RC4 at 40 or 128 bits, and there is no option for AES; the document identifier it generates comes from MD5 over a repr of the clock and of random.random, which is not a cryptographic source. Treat PDF passwords from this library as access friction, not as protection. Finally, expect documentation friction: the Documentation URL in the package metadata points at a readthedocs path that now returns 404, and the surviving docs live under the 3.x branch of the same site.
Patterns
Open a PDF and count its pagesread-pages
from PyPDF2 import PdfReader
reader = PdfReader("input.pdf")
print(len(reader.pages))
first = reader.pages[0]The import itself raises a DeprecationWarning pointing at pypdf. Under pytest with -W error that warning fails the test before any of your code runs.
Pull text out of a pageextract-text
from PyPDF2 import PdfReader
reader = PdfReader("input.pdf")
text = "\n".join(page.extract_text() for page in reader.pages)This walks content streams with no layout model, so multi-column pages interleave and tables lose their structure. It returns an empty string for scanned pages, which have no text at all.
Limit extraction to one text orientationextract-rotated-text
from PyPDF2 import PdfReader
page = PdfReader("input.pdf").pages[0]
upright = page.extract_text(orientations=(0,))
sideways = page.extract_text(orientations=(90,))orientations defaults to all four quarter turns, so rotated stamps and margin notes land in the middle of your body text. Restricting it is usually the quickest cleanup.
Write selected pages to a new filesplit-pages
from PyPDF2 import PdfReader, PdfWriter
reader = PdfReader("input.pdf")
writer = PdfWriter()
for page in reader.pages[0:5]:
writer.add_page(page)
with open("first-five.pdf", "wb") as fh:
writer.write(fh)Open the output in binary mode. write also accepts a path directly and returns a tuple, which trips up code expecting None.
Concatenate several PDFsmerge-documents
from PyPDF2 import PdfWriter
writer = PdfWriter()
for path in ["a.pdf", "b.pdf", "c.pdf"]:
writer.append(path)
writer.write("merged.pdf")PdfWriter.append exists in 3.0.1 and takes a page range and outline options. PdfMerger still works but is the older path and was removed in later pypdf majors.
Turn pages by a quarter turnrotate-pages
from PyPDF2 import PdfReader, PdfWriter
reader = PdfReader("input.pdf")
writer = PdfWriter()
for page in reader.pages:
writer.add_page(page.rotate(90))
writer.write("rotated.pdf")The angle must be a multiple of 90 or it raises. rotate returns the page so it chains; rotateClockwise is the deprecated spelling that still works and warns.
Overlay one page onto anotherstamp-a-watermark
from PyPDF2 import PdfReader, PdfWriter
stamp = PdfReader("watermark.pdf").pages[0]
reader = PdfReader("input.pdf")
writer = PdfWriter()
for page in reader.pages:
page.merge_page(stamp)
writer.add_page(page)
writer.write("stamped.pdf")merge_page draws the stamp on top at the origin with no scaling. Use add_transformation with a Transformation first if the two page sizes differ.
Scale and move page contenttransform-page
from PyPDF2 import PdfReader, PdfWriter, Transformation
page = PdfReader("input.pdf").pages[0]
page.add_transformation(Transformation().scale(0.5, 0.5).translate(100, 200))
writer = PdfWriter()
writer.add_page(page)
writer.write("scaled.pdf")The transformation affects content only; the media box stays the same size, so scaled content sits inside the original page dimensions unless you resize the box too.
Open a password-protected documentread-encrypted
from PyPDF2 import PdfReader, PasswordType
reader = PdfReader("secret.pdf")
if reader.is_encrypted:
result = reader.decrypt("hunter2")
if result == PasswordType.NOT_DECRYPTED:
raise SystemExit("wrong password")
print(len(reader.pages))PdfReader also takes password= directly at construction. AES-encrypted files need the crypto extra, which installs PyCryptodome.
Add a password, and know what it is worthencrypt-output
from PyPDF2 import PdfReader, PdfWriter
writer = PdfWriter()
writer.append_pages_from_reader(PdfReader("input.pdf"))
writer.encrypt(user_password="open-me", owner_password="admin", use_128bit=True)
writer.write("protected.pdf")3.0.1 writes RC4 at 40 or 128 bits only, with no AES option, and the file identifier comes from MD5 over the clock and random.random. This deters casual opening; it is not confidentiality.
Read and rewrite document informationread-metadata
from PyPDF2 import PdfReader, PdfWriter
reader = PdfReader("input.pdf")
print(reader.metadata.title, reader.metadata.author)
writer = PdfWriter()
writer.append_pages_from_reader(reader)
writer.add_metadata({"/Title": "Quarterly report", "/Producer": "internal"})
writer.write("tagged.pdf")Metadata keys are PDF names and need the leading slash. add_metadata merges into whatever the writer already carries rather than replacing it.
Rename the import and drop the warningmigrate-to-pypdf
# pip uninstall PyPDF2 && pip install pypdf
-from PyPDF2 import PdfReader, PdfWriter
+from pypdf import PdfReader, PdfWriter
# same call sites; check any use of PdfMerger,
# which later pypdf majors removed in favour of PdfWriter.appendMost codebases need only the import line. Run the suite with -W error::DeprecationWarning afterwards to catch camelCase methods that pypdf has since removed outright.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| pypdf | PyPI | You want this library under its current name, still released, with AES writing and years of extraction and parsing fixes |
| pymupdf | PyPI | You need rendering, accurate text extraction with positions, or speed on large documents and can accept a compiled dependency and its licence terms |
| pdfplumber | PyPI | You are pulling structured content out of PDFs, especially tables and word positions, rather than editing pages |