Python’s PyPDF2 (or its modern fork pypdf) is the standard library for basic PDF manipulation. You can merge PDFs, split pages, rotate pages, extract text, and add metadata. For more advanced tasks (creating PDFs from scratch, adding images, complex layouts), you need reportlab.
This lesson covers both. You will learn to extract text from PDFs, merge multiple PDFs, split a PDF into separate pages, rotate and crop pages, and create simple PDFs from scratch. These skills are essential for document processing automation.
🕯️ Magic Note
PDF is a complex format. It can contain text, images, vector graphics, fonts, annotations, forms, and more. Extracting text from PDFs is not always reliable because text may be stored as glyphs without Unicode mapping. For scanned PDFs, you need OCR (Optical Character Recognition) with pytesseract and pdf2image.
Bash
# Install pypdf (recommended over PyPDF2)
pip install pypdf
# For creating PDFs from scratch
pip install reportlab
Python
from pypdf import PdfReader, PdfWriter
Python
from pypdf import PdfReader
def extract_text_from_pdf(pdf_path):
“””Extract all text from a PDF file.”””
reader = PdfReader(pdf_path)
# Get number of pages
num_pages = len(reader.pages)
print(f”PDF has {num_pages} pages”)
# Extract text from all pages
full_text = “”
for page_num, page in enumerate(reader.pages, 1):
text = page.extract_text()
full_text += f”\n— Page {page_num} —\n{text}”
return full_text
# Usage
text = extract_text_from_pdf(“document.pdf”)
print(text[:500]) # Print first 500 characters
Python
from pypdf import PdfWriter, PdfReader
def merge_pdfs(pdf_list, output_path):
“””Merge multiple PDF files into one.”””
writer = PdfWriter()
for pdf_path in pdf_list:
reader = PdfReader(pdf_path)
for page in reader.pages:
writer.add_page(page)
print(f”Added: {pdf_path}”)
# Save merged PDF
with open(output_path, “wb”) as f:
writer.write(f)
print(f”Merged {len(pdf_list)} files into {output_path}”)
# merge_pdfs([“doc1.pdf”, “doc2.pdf”, “doc3.pdf”], “merged.pdf”)
🕯️ Magic Note
Merging PDFs is useful for combining scanned documents, consolidating reports, or creating portfolios. You can merge specific pages from each PDF, not just entire documents.
Python
from pypdf import PdfReader, PdfWriter
from pathlib import Path
def split_pdf(input_pdf, output_folder):
“””Split a PDF into separate files, one per page.”””
reader = PdfReader(input_pdf)
output_path = Path(output_folder)
output_path.mkdir(exist_ok=True)
base_name = Path(input_pdf).stem
for page_num, page in enumerate(reader.pages, 1):
writer = PdfWriter()
writer.add_page(page)
output_file = output_path / f”{base_name}_page_{page_num:03d}.pdf”
with open(output_file, “wb”) as f:
writer.write(f)
print(f”Saved: {output_file}”)
print(f”Split {len(reader.pages)} pages into {output_folder}”)
# split_pdf(“large_document.pdf”, “pages”)
Python
from pypdf import PdfReader, PdfWriter
def extract_pages(input_pdf, output_pdf, start_page, end_page=None):
“””Extract a range of pages from a PDF.”””
reader = PdfReader(input_pdf)
writer = PdfWriter()
# Convert to 0-based indexing
start_idx = start_page – 1
end_idx = end_page if end_page else len(reader.pages)
for i in range(start_idx, end_idx):
writer.add_page(reader.pages[i])
with open(output_pdf, “wb”) as f:
writer.write(f)
print(f”Extracted pages {start_page}-{end_page or len(reader.pages)} to {output_pdf}”)
# extract_pages(“document.pdf”, “chapter1.pdf”, start_page=10, end_page=25)
Python
from pypdf import PdfReader, PdfWriter
def rotate_pages(input_pdf, output_pdf, rotation_degrees=90):
“””Rotate all pages in a PDF.”””
reader = PdfReader(input_pdf)
writer = PdfWriter()
for page in reader.pages:
page.rotate(rotation_degrees)
writer.add_page(page)
with open(output_pdf, “wb”) as f:
writer.write(f)
print(f”Rotated all pages by {rotation_degrees} degrees”)
# rotate_pages(“landscape.pdf”, “portrait.pdf”, rotation_degrees=90)
Python
from pypdf import PdfReader, PdfWriter
from datetime import datetime
def add_metadata(input_pdf, output_pdf, metadata):
“””Add metadata to a PDF.”””
reader = PdfReader(input_pdf)
writer = PdfWriter()
# Copy all pages
for page in reader.pages:
writer.add_page(page)
# Add metadata
writer.add_metadata(metadata)
with open(output_pdf, “wb”) as f:
writer.write(f)
print(“Metadata added successfully”)
# Metadata example
metadata = {
“/Title”: “Annual Report 2025”,
“/Author”: “Feloriya Analytics”,
“/Subject”: “Company Performance”,
“/Keywords”: “revenue, growth, metrics”,
“/Creator”: “Feloriya PDF Generator”,
“/Producer”: “Python pypdf”,
“/CreationDate”: f”D:{datetime.now().strftime(‘%Y%m%d%H%M%S’)}”
}
# add_metadata(“report.pdf”, “report_with_metadata.pdf”, metadata)
Python
from reportlab.lib.pagesizes import letter, A4
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib import colors
from reportlab.lib.units import inch
from datetime import datetime
def create_simple_pdf(output_path, title, content_lines):
“””Create a simple PDF document.”””
doc = SimpleDocTemplate(output_path, pagesize=letter)
styles = getSampleStyleSheet()
story = []
# Add title
title_style = styles[“Title”]
story.append(Paragraph(title, title_style))
story.append(Spacer(1, 0.25 * inch))
# Add date
date_style = ParagraphStyle(name=”Date”, parent=styles[“Normal”])
story.append(Paragraph(f”Generated: {datetime.now().strftime(‘%Y-%m-%d %H:%M:%S’)}”, date_style))
story.append(Spacer(1, 0.25 * inch))
# Add content
for line in content_lines:
story.append(Paragraph(line, styles[“Normal”]))
story.append(Spacer(1, 0.1 * inch))
# Build PDF
doc.build(story)
print(f”PDF created: {output_path}”)
# Usage
content = [
“This is a sample PDF generated with Python.”,
“ReportLab makes it easy to create professional-looking documents.”,
“You can add tables, images, charts, and more.”
]
create_simple_pdf(“sample.pdf”, “Python PDF Generation”, content)
🕯️ Magic Note
ReportLab is the industry standard for generating PDFs from Python. It can create complex layouts with tables, charts, images, and vector graphics. PyPDF2 can only manipulate existing PDFs; ReportLab creates them from scratch.
Python
from reportlab.lib.pagesizes import letter
from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer
from reportlab.lib.styles import getSampleStyleSheet
from reportlab.lib import colors
from reportlab.lib.units import inch
def create_table_pdf(output_path, title, headers, data):
“””Create a PDF with a formatted table.”””
doc = SimpleDocTemplate(output_path, pagesize=letter)
styles = getSampleStyleSheet()
story = []
# Add title
story.append(Paragraph(title, styles[“Title”]))
story.append(Spacer(1, 0.25 * inch))
# Create table data (headers + rows)
table_data = [headers] + data
# Create table
table = Table(table_data)
# Style the table
table.setStyle(TableStyle([
(“BACKGROUND”, (0, 0), (-1, 0), colors.grey),
(“TEXTCOLOR”, (0, 0), (-1, 0), colors.whitesmoke),
(“ALIGN”, (0, 0), (-1, -1), “CENTER”),
(“FONTNAME”, (0, 0), (-1, 0), “Helvetica-Bold”),
(“FONTSIZE”, (0, 0), (-1, 0), 12),
(“BOTTOMPADDING”, (0, 0), (-1, 0), 12),
(“BACKGROUND”, (0, 1), (-1, -1), colors.beige),
(“GRID”, (0, 0), (-1, -1), 1, colors.black),
]))
story.append(table)
doc.build(story)
print(f”Table PDF created: {output_path}”)
# Usage
headers = [“Name”, “Age”, “City”]
data = [
[“Ali Rezaei”, “25”, “Tehran”],
[“Sara Mohammadi”, “30”, “Shiraz”],
[“Reza Karimi”, “28”, “Isfahan”]
]
create_table_pdf(“table_report.pdf”, “User Information”, headers, data)
Python
import csv
from reportlab.lib.pagesizes import letter
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib import colors
from reportlab.lib.units import inch
from datetime import datetime
def generate_report_from_csv(csv_path, output_pdf):
“””Generate a PDF report from CSV data.”””
# Read CSV data
with open(csv_path, “r”, encoding=”utf-8″) as f:
reader = csv.DictReader(f)
rows = list(reader)
fieldnames = reader.fieldnames
if not rows:
print(“No data found”)
return
# Create PDF
doc = SimpleDocTemplate(output_pdf, pagesize=letter)
styles = getSampleStyleSheet()
story = []
# Title
story.append(Paragraph(f”Report: {csv_path}”, styles[“Title”]))
story.append(Spacer(1, 0.2 * inch))
story.append(Paragraph(f”Generated: {datetime.now().strftime(‘%Y-%m-%d %H:%M:%S’)}”, styles[“Normal”]))
story.append(Spacer(1, 0.2 * inch))
story.append(Paragraph(f”Total Records: {len(rows)}”, styles[“Normal”]))
story.append(Spacer(1, 0.3 * inch))
# Data table
headers = fieldnames
table_data = [headers]
for row in rows:
table_data.append([row.get(h, “”) for h in headers])
table = Table(table_data)
table.setStyle(TableStyle([
(“BACKGROUND”, (0, 0), (-1, 0), colors.grey),
(“TEXTCOLOR”, (0, 0), (-1, 0), colors.whitesmoke),
(“ALIGN”, (0, 0), (-1, -1), “CENTER”),
(“FONTNAME”, (0, 0), (-1, 0), “Helvetica-Bold”),
(“GRID”, (0, 0), (-1, -1), 1, colors.black),
]))
story.append(table)
doc.build(story)
print(f”Report generated: {output_pdf}”)
# generate_report_from_csv(“data.csv”, “report.pdf”)
Python
# Note: Requires Tesseract OCR installed on system
# pip install pdf2image pytesseract pillow
from pdf2image import convert_from_path
import pytesseract
from PIL import Image
def ocr_scanned_pdf(pdf_path, dpi=300):
“””Extract text from a scanned PDF using OCR.”””
# Convert PDF pages to images
images = convert_from_path(pdf_path, dpi=dpi)
full_text = “”
for i, image in enumerate(images, 1):
# Perform OCR on each page
text = pytesseract.image_to_string(image, lang=”eng”)
full_text += f”\n— Page {i} —\n{text}”
print(f”Processed page {i}”)
return full_text
# text = ocr_scanned_pdf(“scanned_document.pdf”)
- Assuming text extraction always works (fonts, encoding issues)
- Not handling password-protected PDFs (use PdfReader(stream, password=”…”))
- Forgetting that some PDFs have no text (scanned images require OCR)
- Loading large PDFs entirely into memory (process page by page)
- Confusing PyPDF2 with pypdf (pypdf is the modern fork)
- Expecting to edit existing PDFs easily (creating from scratch is easier)
- How do you extract text from a PDF?
- Write code to merge two PDF files into one.
- How do you split a PDF into separate pages?
- What is the difference between pypdf and reportlab?
- How do you handle scanned PDFs (images)?
- Write a function that rotates all pages of a PDF by 90 degrees.
⚡ Whisper
PDFs are containers. They hold text, images, fonts, and metadata. The pypdf library lets you reach inside those containers. Extract text. Merge documents. Split pages. Rotate. Crop. Add metadata. reportlab lets you create new containers from nothing. Add paragraphs. Build tables. Draw shapes. Generate reports. Together, they give you complete control over PDF documents. Automate invoice generation. Process incoming contracts. Extract data from forms. Create weekly reports. The PDF format is complex but Python makes it manageable. Start with extraction. Then merging. Then creation. Your document workflows will thank you.