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

ApproachBest forTradeoff
Calibre CLIProduction batch jobs, top quality, full EPUB support~500MB install
ebooklib + WeasyPrintCustom styling, simpler EPUBs, smaller depsNeed to handle spine/assets manually
ChangeThisFile APINo install, occasional conversions, edge runtimesPer-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.