Splitting a Multi-Document PDF Into Separate Files in Python
Choose the right boundary signal before splitting a multi-document PDF. This guide compares page ranges, bookmarks, and content-aware classification using pypdf, PyMuPDF, and Unsiloed.


You get a single PDF containing 30 invoices, 12 contracts, or 50 patient forms. A bad split can attach one record's pages to another or cut a multi-page document in half. The right method depends on the boundary information the PDF provides: known page ranges, bookmarks, or detectable content changes.
This guide shows how to split PDF files in Python with pypdf and PyMuPDF, then compares custom content-aware splitting with using Unsiloed to detect and assemble category-based outputs.
TL;DR:
- Split a PDF using known page ranges, bookmark or table-of-contents structure, or detected content boundaries.
- Both pypdf, a pure-Python library, and PyMuPDF handle page-range splits.
- Text-based boundary rules need extractable text. Image-only pages usually need optical character recognition (OCR), while visual document systems can inspect the page images directly.
- Page-range libraries need boundaries supplied in advance, while content-aware parsing can detect them.
- Unsiloed is useful when boundaries vary by content or layout and you do not want to build and maintain page classification, grouping, and output assembly yourself.
How to Split PDF Files in Python: Three Strategies
To split a PDF into separate files, start by identifying which boundary signal you can trust, whether that is page ranges, bookmarks or sections, or content patterns.

How to Split a PDF by Page Range in Python
Both pypdf and PyMuPDF can copy a known sequence of pages into a new file, but neither discovers where logical documents begin or end.
Split a PDF by Page Range Using pypdf
pypdf is a pure-Python PDF library. The pypdf project history explains how the PyPDF2 fork merged back into pypdf. Install it with:
pip install pypdf
Add the following function to your PDF-processing script. It copies each specified range from a PdfReader to a new PdfWriter.
from pathlib import Path
from pypdf import PdfReader, PdfWriter
def split_by_ranges(
input_path: str,
ranges: list[tuple[int, int]],
output_dir: str,
) -> None:
reader = PdfReader(input_path)
destination = Path(output_dir)
destination.mkdir(parents=True, exist_ok=True)
for i, (start, end) in enumerate(ranges):
if not 0 <= start < end <= len(reader.pages):
raise ValueError(f"Invalid page range: ({start}, {end})")
writer = PdfWriter()
for page_num in range(start, end):
writer.add_page(reader.pages[page_num])
output_path = destination / f"document_{i + 1}.pdf"
with open(output_path, "wb") as f:
writer.write(f)
print(f"Written: {output_path}")
split_by_ranges("batch.pdf", [(0, 3), (3, 7), (7, 12)], "output")
ranges accepts (start, end) tuples where end is exclusive, matching Python's slice convention. Each iteration builds a fresh writer and leaves the source file untouched. This works when you know the boundaries because pypdf does not discover them.
Split a PDF by Page Range Using PyMuPDF
PyMuPDF gives you precise control over which pages land in each output file. Install it with:
pip install pymupdf
Add the following function to your PDF-processing script to extract a contiguous page range into a standalone PDF.
from pathlib import Path
import pymupdf
def split_pdf_by_range(
input_path: str,
output_path: str,
first_page: int,
last_page: int,
) -> None:
destination = Path(output_path)
destination.parent.mkdir(parents=True, exist_ok=True)
with pymupdf.open(input_path) as source:
if not 0 <= first_page <= last_page < source.page_count:
raise ValueError(
f"Invalid inclusive page range: ({first_page}, {last_page})"
)
with pymupdf.open() as output:
output.insert_pdf(
source,
from_page=first_page,
to_page=last_page,
)
output.save(destination)
split_pdf_by_range("report.pdf", "section_one.pdf", 0, 4)
The PyMuPDF insert_pdf() parameters are zero-based and inclusive, so first_page=0, last_page=4 extracts pages 1 through 5. This differs from the end-exclusive tuples in the pypdf example.
How to Split a PDF by Bookmarks or Table of Contents
When you split PDF files by bookmark, the document's internal outline becomes the split map. Each outline entry has a title and destination, while its position in the tree supplies the heading level. This approach avoids parsing the page content when the structure already exists.
Read the Outline With pypdf
Add the following functions to your PDF-processing script to turn top-level outline entries into split points.
import re
from pathlib import Path
from pypdf import PdfReader, PdfWriter
def iter_outline_items(items, level=1):
for item in items:
if isinstance(item, list):
yield from iter_outline_items(item, level + 1)
else:
yield level, item
def safe_filename(title: str) -> str:
name = re.sub(r"[^A-Za-z0-9._-]+", "_", title).strip("._")
return name[:80] or "untitled"
def split_by_toc(input_path: str, output_dir: str) -> None:
reader = PdfReader(input_path)
page_count = len(reader.pages)
destination = Path(output_dir)
destination.mkdir(parents=True, exist_ok=True)
boundaries = []
seen_pages = set()
for level, item in iter_outline_items(reader.outline):
if level == 1 and hasattr(item, "title"):
page_num = reader.get_destination_page_number(item)
if (
page_num is None
or not 0 <= page_num < page_count
or page_num in seen_pages
):
continue
boundaries.append((item.title, page_num))
seen_pages.add(page_num)
boundaries.sort(key=lambda boundary: boundary[1])
if not boundaries:
raise ValueError("The PDF has no usable top-level bookmarks")
if boundaries[0][1] > 0:
boundaries.insert(0, ("front_matter", 0))
for i, (title, start) in enumerate(boundaries):
end = boundaries[i + 1][1] if i + 1 < len(boundaries) else page_count
writer = PdfWriter()
for p in range(start, end):
writer.add_page(reader.pages[p])
filename = f"{i + 1:02d}_{safe_filename(title)}.pdf"
with open(destination / filename, "wb") as f:
writer.write(f)
split_by_toc("report.pdf", "chapters")
The pypdf outline may contain nested lists. The helper walks that structure, selects level-one destinations, converts them to zero-based indices, and preserves pages before the first bookmark as front_matter. It also generates safe local filenames instead of inserting arbitrary bookmark titles into paths.
Read the Outline With PyMuPDF
Use get_toc() where your script opens the source document:
import pymupdf
with pymupdf.open("report.pdf") as doc:
toc = doc.get_toc() # [[level, title, page_number], ...]
The PyMuPDF get_toc() method returns a flat list of [level, title, page] triples with one-based page numbers. Filter for level == 1, discard entries whose page is -1, and subtract one before passing the page number to insert_pdf().
This approach requires a machine-readable outline. A PDF can display a table of contents on a page without exposing bookmark data, and scanned PDFs may or may not include bookmarks. If reader.outline or get_toc() is empty, use another boundary source.
How Content-Aware PDF Splitting Detects Document Boundaries
When page ranges and outlines are unavailable, inspect the PDF's text and page images before applying rules that infer document boundaries.
Prepare Scanned and Untagged PDFs Before Splitting
Scanned and untagged PDFs need different preprocessing. Check the input before applying content-based rules.
An empty pdfplumber text result does not prove that a page is image-only. It can also indicate missing or unusable character mappings. Inspect representative pages and test whether text can be selected. If the page content is an image, add OCR or use a visual document model. If text extracts but headings have no structural markup, use visual or textual heuristics to propose boundaries.
For scanned files, run OCR with Tesseract through a wrapper such as pytesseract, or use a document service that supports image input. This guide to making a scanned PDF searchable explains how OCR adds the text layer those rules need. For untagged files, font-size changes, whitespace gaps, and repeated headers can signal section starts.
Detect Document Boundaries Automatically
Hardcoded document boundaries work when you control the source. When page counts vary, content-aware splitting uses evidence from the document to propose where one logical unit ends and the next begins.
Possible signals include repeated first-page headers, document identifiers, layout changes, and page-level classification results. Text embeddings can also compare adjacent pages, but a similarity drop is only a candidate boundary, not proof that a new document begins.
Test any rule or model against labeled examples from the documents you actually process. Record the chosen boundaries and source page ranges in your application if downstream systems need to trace an output back to the input.
When to Use Unsiloed Instead of PDF Splitting Code
Libraries remain the simpler option when page ranges or bookmarks provide reliable boundaries. In those cases, pypdf and PyMuPDF only need to copy the selected pages.
Variable-length mixed batches create a different problem. Before a library can write the output files, your application must:
- recover usable text or page images from each input
- choose and maintain signals that distinguish document types
- classify and group pages without cutting a multi-page document apart
- validate new layouts and route uncertain results for review
The Unsiloed Split API replaces that boundary-detection and output-assembly layer. You supply category definitions, and Unsiloed uses page content, structure, and visual characteristics to classify the pages and create a separate PDF for each detected category. This reduces the custom splitting logic your application must maintain when page counts, scans, and layouts vary between batches.

For example, you can define Invoice, Receipt, and Contract categories before submitting a mixed batch. Each category-named output can then enter its own PDF classification and routing workflow, instead of sending contracts and receipts through an invoice extractor. Split jobs run asynchronously, so your application submits the file and then polls the status endpoint for the result.
Each file object in the completed response contains name, path, fileId, full_path, and confidence_score. The confidence score applies to the file's classification, so your application can decide which outputs continue automatically and which need review. Your application still owns the category definitions, acceptance thresholds, and review policy. The Split response does not include field-level confidence scores, word-level citations, or source page ranges. See the Split status response reference for the complete response shape.
How to Choose a PDF Splitting Method
Use pypdf or PyMuPDF when boundaries are known, bookmarks when the PDF supplies a reliable outline, and custom content rules when your layouts are stable enough to maintain them. Use Unsiloed when boundaries depend on changing content or layout and you want the API to handle page classification, grouping, and PDF assembly. Book a demo with Unsiloed AI to test category-based splitting on your own mixed files.
FAQ
These questions cover common decisions that arise when the source PDF does not provide reliable split points.
How do I split PDF documents into multiple files when I don't know the page boundaries in advance?
Use bookmarks if the PDF has an outline. Otherwise, detect repeated headers or identifier patterns, or compare page embeddings to find likely topic transitions.
Can I split a PDF into separate files without knowing whether it's scanned or text-based?
Yes, but inspect it first. Empty text extraction can indicate an image-only page or an encoding problem. Image-only content needs OCR for text-based rules, while text without structural markup calls for other boundary signals.
pypdf vs PyMuPDF for splitting PDFs by page range?
Choose pypdf for a pure-Python dependency and PyMuPDF for its broader PDF tooling and direct page-copy operations. Both need boundaries supplied in advance.
When should I move from a PDF splitting library to an API?
Consider an API when inputs are scanned or mixed-format, categories depend on content, or your application needs downloadable outputs with classification confidence scores.
What is content-aware splitting and when does it produce better results than page-range splitting?
It uses content or layout signals to infer how pages should be grouped. Use it for variable-length batches whose page ranges and bookmarks are unavailable, and validate the results against labeled examples.



