HTML to PDF in Python

Four ways to turn HTML into a PDF from Python, all run on the same invoice template so you can see where each one breaks. Tested with WeasyPrint 67, xhtml2pdf 0.2.17 and Playwright for Python with Chromium.

Quick answer

LibraryPick it when
WeasyPrint
Own layout engine, no browser
Documents you write yourself: invoices, reports. Best print CSS (page numbers, margin boxes), flexbox and grid. No JavaScript.
xhtml2pdf
Pure Python, ReportLab
Simple table layouts where you can't install system libraries. CSS 2.1 only: no flexbox or grid.
Playwright
Real Chromium
Pages that need JavaScript (charts, SPAs), or output identical to Chrome's print. You ship a ~150 MB browser.
An API
Chromium on someone else's server
Chrome-quality output from a server, Lambda or CI job where you don't want to run a browser.

pdfkit isn't on the list: it is a wrapper around wkhtmltopdf, which was archived in 2023 and uses a 2012-era WebKit. See the wkhtmltopdf alternative guide if you're moving off it.

The test template

Save this as invoice.html. It uses flexbox for the header and a CSS paged-media margin box for "Page 1 of 1":

<!doctype html>
<html><head><meta charset="utf-8"><style>
  @page { size: A4; margin: 2cm; @bottom-right { content: "Page " counter(page) " of " counter(pages); } }
  body { font-family: sans-serif; }
  .top { display: flex; justify-content: space-between; }
  table { width: 100%; border-collapse: collapse; }
  td, th { border-bottom: 1px solid #ddd; padding: 6px; text-align: left; }
</style></head><body>
  <div class="top"><h1>Invoice #1042</h1><p>Due: 2026-11-01</p></div>
  <table><tr><th>Item</th><th>Price</th></tr>
    <tr><td>Consulting (3h)</td><td>€450.00</td></tr>
    <tr><td>Hosting, Oct.</td><td>€19.00</td></tr></table>
</body></html>

1. WeasyPrint

pip install weasyprint

On Linux it also needs Pango (apt install weasyprint pulls in everything on Debian/Ubuntu); on macOS, brew install weasyprint; on Windows, follow its docs (GTK runtime) or use the standalone executable.

from weasyprint import HTML

HTML("invoice.html").write_pdf("invoice.pdf")

# From a string or a URL instead of a file:
HTML(string="<h1>Hello, PDF</h1>").write_pdf("hello.pdf")
HTML(url="https://example.com").write_pdf("example.pdf")

Result: an A4 PDF with the title and due date side by side and "Page 1 of 1" at the bottom right, in under a second. WeasyPrint handles @page rules, margin boxes, counter(pages), break-inside: avoid, flexbox and grid. What it doesn't do is run JavaScript: a <script> that fills in a chart or a total is simply ignored. Relative image and CSS paths resolve against the file; for HTML(string=...) pass base_url=.

2. xhtml2pdf

pip install xhtml2pdf
from xhtml2pdf import pisa

with open("invoice.html", encoding="utf-8") as src, open("invoice-x.pdf", "wb") as out:
    result = pisa.CreatePDF(src.read(), dest=out, encoding="utf-8")

if result.err:
    print("failed:", result.err)

Run on our template, this crashes with TypeError: 'NotImplementedType' object is not iterable inside its CSS parser: xhtml2pdf doesn't understand margin boxes like @bottom-right. Remove that part of the @page rule and it produces a PDF, but the flexbox header is ignored (the due date drops below the title), and grid columns run together. It's pure Python (it draws with ReportLab), so it installs anywhere, which is its real selling point. Page numbers use its own @frame and <pdf:pagenumber> syntax instead of standard CSS.

3. Playwright (headless Chromium)

pip install playwright
playwright install chromium
from pathlib import Path
from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch()
    page = browser.new_page()
    page.goto(Path("invoice.html").resolve().as_uri())  # or any https:// URL
    page.pdf(path="invoice-pw.pdf", format="A4", print_background=True,
             margin={"top": "2cm", "bottom": "2cm", "left": "2cm", "right": "2cm"})
    browser.close()

Result: the same layout as in Chrome, including "Page 1 of 1" (recent Chromium supports @page margin boxes), and any JavaScript runs before printing. For HTML in a string, use page.set_content(html, wait_until="networkidle"). Things to know:

Prefer Selenium? Its print_page works too: see Selenium screenshots and PDFs in Python.

4. An API (no browser to install)

Shotmint renders in the same Chromium as Playwright, so the output matches, but your Python process only makes an HTTP call. Good for Lambda, small containers and cron jobs:

import os, requests

html = open("invoice.html", encoding="utf-8").read()
r = requests.post("https://shotmint.creasework.com/v1/pdf",
                  headers={"X-API-Key": os.environ["SHOTMINT_KEY"]},
                  json={"html": html, "format": "A4", "margin": "2cm"},
                  timeout=60)
r.raise_for_status()
open("invoice-api.pdf", "wb").write(r.content)

For a live page, send "url": "https://..." instead of html. Other options: landscape, format (A3, A4, A5, Letter, Legal, Tabloid), scale, print_background (on by default) and screen_media to print with screen CSS. Images and fonts must be reachable from the internet (or inlined as data URIs). More in the Python API guide and the HTML invoice guide.

Side by side

FeatureSupport
Flexbox / gridWeasyPrint yes, Playwright yes, API yes
xhtml2pdf: no (ignored)
JavaScriptPlaywright yes, API yes
WeasyPrint and xhtml2pdf: ignored
"Page X of Y" in CSSWeasyPrint yes, Playwright yes, API yes
xhtml2pdf: crashes on margin boxes; use its own tags
Installxhtml2pdf: pip only
WeasyPrint: plus Pango; Playwright: plus Chromium; API: requests
Speed (our template)xhtml2pdf ~0.4 s, WeasyPrint ~0.9 s, Playwright ~1.3 s with launch
API: one network round trip

Which one?

If you control the HTML and it has no JavaScript, start with WeasyPrint: it has the best print CSS and no browser. If the page needs JavaScript or must look exactly like Chrome, use Playwright, or the API if you'd rather not run Chromium yourself. Use xhtml2pdf only for simple layouts where pip-only installation matters most. Want to see the Chromium output with no code? Paste your HTML into the free HTML to PDF converter. Node.js instead? See HTML to PDF in Node.js. Need a PNG instead of a PDF? See HTML to image in Python.

Try it with 100 free renders

No signup, no card. Get a key in one click, then buy credits only if you need more (from €5 for 2,000, never expire).

Get a free API key Full API reference