EPUB-to-PDF is harder than it looks. EPUB is a zip of HTML files; PDF needs strict pagination, embedded fonts, and consistent margins. Calibre's ebook-convert is the most battle-tested option — it understands EPUB quirks the simpler tools don't. The pure-Python options work for clean EPUBs but break on edge cases.
Method 1: Calibre ebook-convert (best output quality)
Calibre is the gold-standard ebook converter. It handles every EPUB feature, generates proper TOCs, embeds fonts, and produces print-ready PDFs.
# Install Calibre system-wide:
# macOS: brew install --cask calibre
# Ubuntu: apt install calibre
# Or download: https://calibre-ebook.com/download
import subprocess
def epub_to_pdf(in_path: str, out_path: str, paper_size: str = "a4") -> None:
subprocess.run(
[
"ebook-convert", in_path, out_path,
"--paper-size", paper_size, # a4, letter, b5, etc.
"--pdf-page-numbers",
"--pdf-default-font-size", "11",
"--pretty-print",
],
check=True,
)
epub_to_pdf("book.epub", "book.pdf")
epub_to_pdf("book.epub", "book_letter.pdf", paper_size="letter")
Useful flags:
- --paper-size — a4 (default), letter, b5, a5, legal.
- --pdf-page-numbers — adds page numbers to the footer.
- --pdf-default-font-size — base font size for body text. 11 is good for screen reading; 14 for print.
- --pdf-serif-family / --pdf-sans-family — override the default fonts (must be installed on the system).
Calibre is heavyweight (~500MB install). If you only need EPUB-to-PDF, that's a lot of dependency. Use it when you need top-quality output.
Method 2: ebooklib + WeasyPrint (pure Python)
If you can't install Calibre, the lighter option is ebooklib (read EPUB) + WeasyPrint (HTML to PDF). Smaller install, more control over styling.
pip install EbookLib weasyprint
from ebooklib import epub, ITEM_DOCUMENT
from weasyprint import HTML, CSS
from pathlib import Path
import tempfile
import os
def epub_to_pdf(in_path: str, out_path: str) -> None:
book = epub.read_epub(in_path)
# Concatenate all chapter HTML in spine order
html_parts = []
for spine_item in book.spine:
item = book.get_item_with_id(spine_item[0])
if item and item.get_type() == ITEM_DOCUMENT:
html_parts.append(item.get_content().decode("utf-8", errors="replace"))
full_html = "" + "\n".join(html_parts) + ""
css = CSS(string="""
@page { size: A4; margin: 2cm; }
body { font-family: 'Times New Roman', serif; line-height: 1.6; font-size: 11pt; }
h1, h2, h3 { page-break-after: avoid; }
""")
HTML(string=full_html).write_pdf(out_path, stylesheets=[css])
epub_to_pdf("book.epub", "book.pdf")
WeasyPrint is excellent for HTML-to-PDF but doesn't understand EPUB's spine, manifest, or embedded resources. Images, fonts, and stylesheets inside the EPUB need extra handling — the snippet above is the minimum case. For real EPUBs you'll need to extract assets, rewrite paths, and merge the EPUB's CSS.
Method 3: ChangeThisFile API (no install, uses Calibre server-side)
Skip the 500MB Calibre install — the API runs Calibre's ebook-convert server-side. Get a free API key.
import requests
API_KEY = "ctf_sk_your_key_here"
def epub_to_pdf(in_path: str, out_path: str) -> None:
with open(in_path, "rb") as f:
response = requests.post(
"https://changethisfile.com/v1/convert",
headers={"Authorization": f"Bearer {API_KEY}"},
files={"file": f},
data={"source": "epub", "target": "pdf"},
timeout=300, # ebook-convert can take 30-60s for big books
)
response.raise_for_status()
with open(out_path, "wb") as out:
out.write(response.content)
epub_to_pdf("book.epub", "book.pdf")
Same Calibre engine, same output quality, no local install. The server uses A4 by default; pass paper_size=letter (or other Calibre-supported values) in the form data to override.
When to use each
| Approach | Best for | Tradeoff |
|---|---|---|
| Calibre CLI | Production batch jobs, top quality, full EPUB support | ~500MB install |
| ebooklib + WeasyPrint | Custom styling, simpler EPUBs, smaller deps | Need to handle spine/assets manually |
| ChangeThisFile API | No install, occasional conversions, edge runtimes | Per-call cost, file size limit |
CLI alternative: pandoc
pandoc can also do EPUB-to-PDF, going via LaTeX:
apt install pandoc texlive-xetex
pandoc book.epub -o book.pdf --pdf-engine=xelatex -V geometry:a4paper
pandoc + xelatex is great for academic books and anything where typography matters more than fidelity. Calibre is better for novels and reference books where layout and TOC matter most.
Production tips
- Calibre is single-instance per machine. Don't run multiple ebook-convert processes against the same Calibre library — they conflict on the SQLite metadata DB. For batch jobs, use --library-path to a per-job directory.
- Big books take time. A 500-page EPUB takes 20-60 seconds to convert with Calibre. Plan timeouts accordingly (300s is safe).
- WeasyPrint needs system Pango/HarfBuzz. apt install libpango1.0-0 libpangoft2-1.0-0. Without these, conversion fails with confusing errors.
- Image-heavy EPUBs balloon in PDF size. A 5MB EPUB with full-page images can become a 50MB PDF. Use --pdf-default-font-size and --pdf-serif-family to keep text-heavy books reasonable.
For production: shell out to Calibre. For pure Python: ebooklib + WeasyPrint with extra asset handling. To skip the install: the API. Free tier is 100 conversions/month, no card.