Quick answer: install pypdf, read documents with PdfReader, assemble output with PdfWriter, and write to a new file. The library merges, splits, and rotates pages, handles metadata, attempts text extraction, and adds encryption. It is neither an OCR engine nor a visual PDF editor.

python -m pip install pypdf

A PDF describes where elements appear on a page; it is not simply a text document. That distinction explains both the useful operations and the extraction limitations. Create a clean Python virtual environment first and use the patterns in our Python pathlib guide for filesystem work. API details and version notes are available in the official pypdf documentation.

Merge PDFs in a predictable order

Pass an explicit list instead of trusting filesystem order. The function below validates every input, refuses to overwrite an input accidentally, creates the output directory, and rejects encrypted files that need credentials.

from pathlib import Path
from pypdf import PdfReader, PdfWriter

def merge_pdfs(inputs: list[Path], output: Path) -> None:
    if not inputs:
        raise ValueError("Provide at least one PDF")
    output = output.resolve()
    writer = PdfWriter()

    for path in inputs:
        path = path.resolve()
        if path == output:
            raise ValueError("Output cannot also be an input")
        if path.suffix.lower() != ".pdf" or not path.is_file():
            raise ValueError(f"Invalid PDF: {path}")
        reader = PdfReader(path)
        if reader.is_encrypted:
            raise ValueError(f"Protected PDF: {path.name}")
        for page in reader.pages:
            writer.add_page(page)

    output.parent.mkdir(parents=True, exist_ok=True)
    with output.open("wb") as stream:
        writer.write(stream)

merge_pdfs(
    [Path("contract.pdf"), Path("appendix.pdf")],
    Path("output/complete-document.pdf"),
)

For batches, build the list with an explicit rule such as sorted(folder.glob("*.pdf")) and log the actual files. Never turn an untrusted filename directly into an output path. This operation can become one stage in a broader Python task automation workflow.

Split pages and rotate selected content

Splitting creates one writer for every page or range. Python indexes start at zero, while filenames intended for users generally start at one. Rotation uses multiples of 90 degrees.

from pathlib import Path
from pypdf import PdfReader, PdfWriter

def split_pdf(source: Path, folder: Path) -> list[Path]:
    reader = PdfReader(source)
    folder.mkdir(parents=True, exist_ok=True)
    created = []
    for number, page in enumerate(reader.pages, start=1):
        writer = PdfWriter()
        writer.add_page(page)
        destination = folder / f"page-{number:03d}.pdf"
        with destination.open("wb") as stream:
            writer.write(stream)
        created.append(destination)
    return created

reader = PdfReader("input.pdf")
writer = PdfWriter()
for index, page in enumerate(reader.pages):
    if index == 0:
        page.rotate(90)
    writer.add_page(page)
with open("rotated.pdf", "wb") as stream:
    writer.write(stream)

Rotation changes page orientation but does not automatically repair crop boxes, annotation coordinates, or form layouts. Open representative portrait and landscape pages before approving a batch.

Inspect and replace metadata

Title, author, and subject are useful for catalogs but may be missing or inaccurate. Never use PDF metadata as an authorization or identity source.

from pypdf import PdfReader, PdfWriter

reader = PdfReader("report.pdf")
print(reader.metadata.title if reader.metadata else None)

writer = PdfWriter()
writer.append_pages_from_reader(reader)
writer.add_metadata({
    "/Title": "Monthly report",
    "/Author": "Data Team",
    "/Subject": "Internal consolidation",
})
with open("cataloged-report.pdf", "wb") as stream:
    writer.write(stream)

Rewriting may not preserve every advanced feature. Digital signatures, embedded files, JavaScript, forms, outlines, and accessibility tags need dedicated tests. Keep the original and compare output rather than replacing the only copy.

Extract text, with realistic expectations

extract_text() recovers characters already represented in the PDF structure. A scan contains pixels, not characters, so it can return nothing; pypdf does not perform OCR. Columns, tables, ligatures, formulas, custom encodings, and absolute positioning may also produce surprising reading order. The official guide explains in more depth why PDF text extraction is difficult.

from pypdf import PdfReader

reader = PdfReader("report.pdf")
sections = []
for number, page in enumerate(reader.pages, start=1):
    text = page.extract_text() or ""
    if not text.strip():
        print(f"Page {number}: no text layer; OCR may be required")
    sections.append(text)

content = "\n\n".join(sections)
print(content[:500])

Keep page numbers and validate important fields against the original. After extraction, simple patterns can be normalized using ideas from our Python regular expressions guide, but regex is not a universal table parser.

Encrypt and open a protected PDF

Read passwords from environment variables or a secret manager, never source code. Do not print them in logs or error reports.

import os
from pypdf import PdfReader, PdfWriter

password = os.environ.get("PDF_PASSWORD")
if not password:
    raise RuntimeError("Set PDF_PASSWORD")

reader = PdfReader("private.pdf")
writer = PdfWriter()
writer.append_pages_from_reader(reader)
writer.encrypt(password, algorithm="AES-256")
with open("private-protected.pdf", "wb") as stream:
    writer.write(stream)

protected = PdfReader("private-protected.pdf")
if protected.is_encrypted and protected.decrypt(password) == 0:
    raise ValueError("Incorrect password")
print(len(protected.pages))

Some algorithms can require an extra cryptography dependency, depending on the installed release. Check current pypdf documentation rather than assuming support. Encryption does not stop an authorized reader from redistributing an opened copy, and it does not replace storage access controls.

Practical project: a report processor

Create an input folder, sort reports by an agreed naming convention, merge them, add metadata, rotate known pages, and initially write to a temporary output. Reopen that file and verify its expected page count before atomically renaming it. Record input names, timestamps, and outcomes using the practices in our Python logging tutorial.

Keep reading, transformation, and writing in separate functions so tests can use tiny fixture PDFs. Set file-size and page-count limits for uploads, reject unexpected extensions, and process untrusted documents in an isolated environment. A small input file may expand substantially in memory because compressed streams must be decoded.

A PDF page has several boxes, including media, crop, bleed, trim, and art boxes. The crop box controls the area normally displayed, while the media box describes the physical page. Merging files created by different tools can therefore produce pages that appear unexpectedly clipped or have inconsistent sizes. Inspect these boxes before normalizing them, because changing dimensions without transforming the content can hide or displace information. The official pypdf guide to cropping and transformations documents the available operations.

Annotations and interactive forms deserve the same caution. A visible link may be stored as an annotation, and a form field can depend on document-level structures in addition to the page where it appears. Copying individual pages is not always equivalent to copying the complete document. If links, bookmarks, attachments, or AcroForm fields are requirements, create an acceptance fixture containing each feature and verify the result in more than one PDF viewer. Flattening a form or preserving a digital signature is a separate requirement, not an automatic side effect of writing with PdfWriter.

Design tests around document invariants

A reliable test does not compare two complete PDF byte streams. Metadata timestamps, object numbering, compression, and serialization details can change even when documents look identical. Assert invariants that matter to the workflow: page count, page dimensions, rotation, selected metadata, encryption state, and the presence of expected text on known pages. Then render a small set of representative outputs for visual comparison when layout matters.

Include malformed, empty, encrypted, unusually large, portrait, and landscape fixtures. Give every test its own temporary directory so parallel runs cannot overwrite each other. For production batches, calculate an expected output count before processing and reconcile it afterward. These checks make partial failures visible and support safe retries without silently duplicating pages.

Common errors and deployment checklist

  • PdfReadError: the file may be corrupted, incomplete, or unsupported; record its name and preserve it for inspection.
  • Wrong password: check is_encrypted and the result from decrypt before reading pages.
  • Empty text: determine whether a text layer exists before introducing an OCR stage.
  • Truncated output: write in binary mode, close the stream, and validate by reopening the result.
  • High memory use: reject oversized or abnormally complex documents before processing.

Before deployment, confirm deterministic ordering, preserved originals, separate input and output paths, secrets outside Git, bounded file sizes and page counts, logs without sensitive contents, verified page totals, visual samples, and pinned current dependencies. Use specific exceptions rather than a broad silent catch; our Python error-handling guide explains why.

Final perspective

pypdf provides a focused API for structural PDF automation. Start with disposable copies, verify every transformation, and treat extracted text as an approximation determined by the source document. Path validation, limits, logs, and tests turn these small operations into a dependable processor without claiming capabilities the format or library cannot provide.