Introduction
Everyone has merged PDFs with sketchy free websites that upload your documents to who-knows-where. This tutorial builds the same tool locally: upload several PDFs, see their page counts, reorder them, and download one combined file — with nothing ever leaving your machine. It uses pypdf, the standard pure-Python PDF library, and pairs nicely with the file organizer for keeping your documents tidy afterwards.
The core API is tiny — a PdfWriter that accepts pages from PdfReader objects — so the interesting engineering is in the UX: multi-upload ordering and streaming a proper download.
Features
st.download_button.Prerequisites
pip install streamlit pypdfStep 1: Create the Script
Save as pdf_merger.py:
import streamlit as st
from pypdf import PdfReader, PdfWriter
from io import BytesIO
st.set_page_config(page_title="PDF Merger", page_icon="📄")
st.title("📄 PDF Merger")
uploads = st.file_uploader(
"Upload PDFs (in any order — reorder below)",
type=["pdf"],
accept_multiple_files=True,
)
if uploads:
# Show page counts and allow reordering
st.subheader("Files & order")
for i, f in enumerate(uploads):
pages = len(PdfReader(BytesIO(f.getvalue())).pages)
c1, c2, c3, c4 = st.columns([0.6, 0.13, 0.13, 0.14])
c1.markdown(f"**{i + 1}. {f.name}** — {pages} pages")
if c2.button("↑", key=f"up{i}", disabled=i == 0):
uploads[i - 1], uploads[i] = uploads[i], uploads[i - 1]
st.rerun()
if c3.button("↓", key=f"down{i}", disabled=i == len(uploads) - 1):
uploads[i + 1], uploads[i] = uploads[i], uploads[i + 1]
st.rerun()
c4.button("✕", key=f"del{i}")
if st.button("🔗 Merge PDFs", type="primary"):
writer = PdfWriter()
total = 0
for f in uploads:
reader = PdfReader(BytesIO(f.getvalue()))
for page in reader.pages:
writer.add_page(page)
total += 1
buffer = BytesIO()
writer.write(buffer)
st.success(f"Merged {len(uploads)} files ({total} pages)")
st.download_button(
"⬇️ Download merged PDF",
data=buffer.getvalue(),
file_name="merged.pdf",
mime="application/pdf",
)Step 2: Run the App
streamlit run pdf_merger.pyUpload three PDFs, reorder one, merge, and open the result — pages appear exactly in your chosen order.
How It Works
pypdf treats a PDF as a container of page objects. PdfReader parses a file and exposes .pages; PdfWriter collects pages from any number of readers and writes them into a fresh PDF. Because merging happens page-by-page rather than file-by-file, the same loop is where you would later insert page ranges, rotations, or watermarks.
The BytesIO buffer appears twice, doing two jobs: wrapping each upload's bytes so pypdf can read them (it expects a file-like object, not raw bytes), and holding the output so st.download_button can serve it. This in-memory pattern — no temp files anywhere — is the same one used in the QR generator.
Reordering is plain Python: swap adjacent items in the upload list and call st.rerun() so the UI reflects the new order. Note the key on every button — without unique keys Streamlit cannot tell the ten reorder buttons apart.
Common Errors & Fixes
reader.is_encrypted and skip with a warning (or call reader.decrypt("") for owner-password-only files).buffer.getvalue(), not buffer.pypdf can't recompress images — use Ghostscript if size matters).Key Concepts
What to Try Next
page.rotate(90).FAQ
Do my files get uploaded anywhere?
No — everything happens in the browser session and your Python process's memory. That is the entire point of running this locally instead of using an online merger.
Does pypdf preserve bookmarks and links?
Internal links and forms mostly survive; document-level outlines (bookmarks) do not merge automatically. pypdf's append() (instead of per-page add_page) preserves more structure when you don't need per-page control.
How large can the files be?
Streamlit caps uploads (default 200 MB, configurable via server.maxUploadSize). Memory is the real limit — each PDF is held in RAM during the merge.