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.
| Library | Pick 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.
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>
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=.
pip install xhtml2pdffrom 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.
pip install playwright
playwright install chromiumfrom 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:
print_background=True is off by default, so background colors and images vanish without it.@page { size } wins over format= only if you pass prefer_css_page_size=True.page.wait_for_selector("canvas") or wait_until="networkidle".mcr.microsoft.com/playwright/python image, or run playwright install --with-deps chromium. Boxes instead of text mean missing fonts (apt install fonts-noto-core fonts-noto-cjk).launch() costs about a second. In a web app, keep one browser open and open a new page per request.Prefer Selenium? Its print_page works too: see Selenium screenshots and PDFs in Python.
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.
| Feature | Support |
|---|---|
| Flexbox / grid | WeasyPrint yes, Playwright yes, API yes xhtml2pdf: no (ignored) |
| JavaScript | Playwright yes, API yes WeasyPrint and xhtml2pdf: ignored |
| "Page X of Y" in CSS | WeasyPrint yes, Playwright yes, API yes xhtml2pdf: crashes on margin boxes; use its own tags |
| Install | xhtml2pdf: 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 |
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.
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