Excel and document tasks

Merge PDFs and extract a page range with pypdf

Merge several PDFs into one file, extract an inclusive page range into a second file, and verify the resulting page counts and order. A synthetic set of blank pages with deliberately different dimensions makes the result checkable by hand.

Show contents

Who this is forThis guide is for people who need to combine PDFs or extract selected pages without modifying the original documents.

What you need
  • Python 3.12 and a terminal command that starts that version.
  • pypdf installed with python -m pip install pypdf.
  • A working folder where the scripts can create folders beneath outputs.
  • Enough disk space for the source PDFs, merged PDF, and extracted PDF.

01Define the merge and extraction rules

The workflow uses three source PDFs in an explicit order: alpha.pdf, beta.pdf, then gamma.pdf. Their pages are appended to one merged PDF. A second output then extracts pages 2 through 5 from the merged PDF, using ordinary human page numbering where the first page is page 1.

Python sequences use zero-based indexes, but this script accepts START_PAGE and END_PAGE as one-based inclusive page numbers. Pages 2 through 5 therefore correspond to Python slice positions 1 through 4, implemented as merged_reader.pages[START_PAGE - 1:END_PAGE].

02Create three synthetic PDFs

The PDFs below are synthetic and were created specifically for this article. Each page is blank, but every page has a different width and height. That makes page order independently checkable without requiring another PDF-generation package.

python
from pathlib import Path
from pypdf import PdfWriter

SOURCE_DIR = Path("outputs") / "pdf_merge_split_demo"
PDFS = {
    "alpha.pdf": [(100, 200), (110, 210)],
    "beta.pdf": [(120, 220)],
    "gamma.pdf": [(130, 230), (140, 240), (150, 250)],
}

SOURCE_DIR.parent.mkdir(parents=True, exist_ok=True)
SOURCE_DIR.mkdir()  # Stop if the synthetic source already exists.

for filename, page_sizes in PDFS.items():
    writer = PdfWriter()
    for width, height in page_sizes:
        writer.add_blank_page(width=width, height=height)
    target = SOURCE_DIR / filename
    with target.open("xb") as stream:
        writer.write(stream)
text
python create_pdf_demo.py
Source PDFPagesPage dimensions in order
alpha.pdf2100×200, 110×210
beta.pdf1120×220
gamma.pdf3130×230, 140×240, 150×250

The three source files contain 6 pages in total: 2 + 1 + 3. The dimensions are expressed in PDF user-space units. Their purpose here is simply to give every synthetic page a recognizable signature.

03Work out the expected page order

Because the inputs are explicitly listed, the merged PDF should contain the two alpha pages first, followed by the beta page, then the three gamma pages. The merged result should therefore contain 6 pages.

Merged pageSourceExpected dimensions
1alpha.pdf page 1100×200
2alpha.pdf page 2110×210
3beta.pdf page 1120×220
4gamma.pdf page 1130×230
5gamma.pdf page 2140×240
6gamma.pdf page 3150×250

Extracting merged pages 2 through 5 should therefore produce exactly 4 pages with dimensions 110×210, 120×220, 130×230, and 140×240 in that order.

04Merge the files and extract pages 2 through 5

Save the following script as pdf_merge_split.py. It validates every input file, rejects encrypted PDFs for this simple example, merges all pages, reopens the merged output to verify it, then creates the requested page-range file.

python
from pathlib import Path
from pypdf import PdfReader, PdfWriter

SOURCE_DIR = Path("outputs") / "pdf_merge_split_demo"
INPUTS = ("alpha.pdf", "beta.pdf", "gamma.pdf")
OUTPUT_DIR = Path("outputs") / "pdf_merge_split_result"
MERGED = OUTPUT_DIR / "merged.pdf"
EXTRACTED = OUTPUT_DIR / "pages_2_to_5.pdf"
START_PAGE = 2
END_PAGE = 5


def page_size(page) -> tuple[int, int]:
    width = int(float(page.mediabox.width))
    height = int(float(page.mediabox.height))
    return width, height


def main() -> None:
    source_root = SOURCE_DIR.resolve(strict=True)
    if not source_root.is_dir():
        raise ValueError("SOURCE_DIR must be a directory.")
    if START_PAGE < 1 or END_PAGE < START_PAGE:
        raise ValueError("Invalid page range.")

    input_paths = [source_root / name for name in INPUTS]
    for path in input_paths:
        if not path.is_file():
            raise FileNotFoundError(f"Missing input PDF: {path}")

    OUTPUT_DIR.parent.mkdir(parents=True, exist_ok=True)
    OUTPUT_DIR.mkdir()  # Refuse to reuse an existing result folder.

    expected_sizes = []
    writer = PdfWriter()
    for path in input_paths:
        reader = PdfReader(path)
        if reader.is_encrypted:
            raise ValueError(f"Encrypted PDF is not supported here: {path.name}")
        for page in reader.pages:
            expected_sizes.append(page_size(page))
            writer.add_page(page)

    with MERGED.open("xb") as stream:
        writer.write(stream)

    merged_reader = PdfReader(MERGED)
    if merged_reader.is_encrypted:
        raise RuntimeError("Unexpected encrypted merged PDF.")
    merged_sizes = [page_size(page) for page in merged_reader.pages]
    if merged_sizes != expected_sizes:
        raise RuntimeError("Merged page count or order does not match the inputs.")

    if END_PAGE > len(merged_reader.pages):
        raise ValueError(
            f"Requested page {END_PAGE}, but merged PDF has only "
            f"{len(merged_reader.pages)} pages."
        )

    selected_pages = merged_reader.pages[START_PAGE - 1:END_PAGE]
    extracted_writer = PdfWriter()
    expected_extract_sizes = []
    for page in selected_pages:
        expected_extract_sizes.append(page_size(page))
        extracted_writer.add_page(page)

    with EXTRACTED.open("xb") as stream:
        extracted_writer.write(stream)

    extracted_reader = PdfReader(EXTRACTED)
    extracted_sizes = [page_size(page) for page in extracted_reader.pages]
    if extracted_sizes != expected_extract_sizes:
        raise RuntimeError("Extracted PDF does not match the selected pages.")

    print(f"Merged {len(INPUTS)} PDFs into {len(merged_reader.pages)} pages.")
    print(
        f"Extracted pages {START_PAGE}-{END_PAGE}: "
        f"{len(extracted_reader.pages)} pages."
    )
    print(f"Merged output: {MERGED.as_posix()}")
    print(f"Extracted output: {EXTRACTED.as_posix()}")


if __name__ == "__main__":
    main()
text
python pdf_merge_split.py

05Compare the expected outputs

The first output, merged.pdf, should contain 6 pages. The second output, pages_2_to_5.pdf, should contain 4 pages. The extraction is inclusive at both ends because the script converts the human page numbers into the corresponding Python slice.

OutputExpected pagesExpected dimensions in order
merged.pdf6100×200, 110×210, 120×220, 130×230, 140×240, 150×250
pages_2_to_5.pdf4110×210, 120×220, 130×230, 140×240

The expected console text below was derived manually from the synthetic inputs and code. It is not an execution log.

text
Merged 3 PDFs into 6 pages.
Extracted pages 2-5: 4 pages.
Merged output: outputs/pdf_merge_split_result/merged.pdf
Extracted output: outputs/pdf_merge_split_result/pages_2_to_5.pdf

06Check the result before using real PDFs

  1. Open merged.pdf and confirm that it has 6 pages.
  2. Open pages_2_to_5.pdf and confirm that it has exactly 4 pages.
  3. Inspect page dimensions or another visible page feature to confirm that the order is 110×210, 120×220, 130×230, 140×240 in the extracted file.
  4. Confirm that alpha.pdf, beta.pdf, and gamma.pdf still exist unchanged in the source folder.
  5. Run the script again without changing OUTPUT_DIR. It should stop with FileExistsError instead of replacing the previous PDFs.

For real documents, also inspect page content visually. Matching counts alone cannot reveal every possible issue, such as an unexpected input order or a source PDF whose page labels differ from its physical page positions.

07Recognize common errors

SymptomWhat to check
ModuleNotFoundError: No module named pypdfInstall pypdf into the same Python environment with python -m pip install pypdf.
FileNotFoundErrorCheck SOURCE_DIR, INPUTS, and the terminal's working directory. Run the synthetic setup script first for this example.
FileExistsErrorThe result folder already exists. Review the previous outputs and choose a new folder rather than overwriting them.
Encrypted PDF is not supported hereUse a permitted decrypted copy or add an explicit password-handling workflow appropriate to your documents.
Requested page is beyond the merged PDFCheck the merged page count and the one-based START_PAGE and END_PAGE values.
Merged or extracted verification mismatchTreat the generated file as unverified. Check the inputs and use a fresh output folder for the next run.

If an exception happens after OUTPUT_DIR is created, partial files can remain. The script does not automatically delete them. Keep failed outputs separate from verified files.

08Understand the limits

This workflow verifies physical page count and page dimensions, not semantic content. It does not compare rendered pixels, extracted text, annotations, forms, bookmarks, named destinations, attachments, signatures, or accessibility structure.

PDF page labels can differ from physical page positions. A document might display roman numerals or custom labels even though pypdf still accesses its page objects by zero-based sequence position. The START_PAGE and END_PAGE values in this tutorial refer to physical sequence positions, not printed page numbers.

Some complex PDFs contain interactive forms, digital signatures, encrypted content, unusual object relationships, or features that require more deliberate handling. Merging a digitally signed PDF into a new document does not preserve the meaning of the original signature as a signature over the newly created file. For important records, inspect the relevant PDF requirements before treating a merged copy as equivalent to the original.

Execution and verification record

2026-09-20 · hand-checked example · target: Python 3.12 · pypdf required · no execution

  • Manually counted source page totals of 2, 1, and 3 for 6 merged pages.
  • Manually traced the expected merged dimensions as 100x200, 110x210, 120x220, 130x230, 140x240, and 150x250.
  • Manually converted the inclusive human page range 2-5 into Python positions 1 through 4 and confirmed that it contains 4 pages.
  • Manually derived the extracted dimensions as 110x210, 120x220, 130x230, and 140x240.
  • Inspected the code to confirm that source PDFs are opened for reading, outputs go to a separate folder, and an existing output folder causes the run to stop.
  • Inspected the page-count and page-dimension comparison logic and derived the expected console output by hand.
Verification limits
  • The code was not executed by the author of this response; no PDF files were created, merged, extracted, or opened here.
  • Encrypted PDFs, forms, annotations, bookmarks, digital signatures, attachments, page labels, and malformed PDFs were not tested.
  • The page-dimension checks verify order for the synthetic example but do not prove semantic equivalence for arbitrary real PDFs.
  • The official documentation URLs were provided from known documentation locations but were not checked live.

Site-wide writing and verification principles

References

The explanations and examples were written for this site. See the official sources below for the related behavior and concepts.