How to convert HTML to PDF in Python
Three ways that work, with code: WeasyPrint (pure Python, no browser), Playwright (real Chrome), or an API call that returns the PDF.
By Saurav N, senior software engineer · Updated
· 6 min readWeasyPrint: no browser needed
WeasyPrint lays out HTML and CSS itself, with strong support for print CSS: @page, running headers and footers, and page counters. It needs Pango and its system libraries installed.
It does not run JavaScript, and some newer layout CSS behaves differently from Chrome, so check documents that use flexbox or grid heavily.
from weasyprint import HTML
html = "<h1>Invoice INV-2026-0142</h1><p>Total ₹49,402.50</p>"
HTML(string=html).write_pdf("invoice.pdf")Playwright: the same output as Chrome
Playwright prints with Chromium, so the PDF matches what you see in the browser, JavaScript included. You carry the cost of running a browser.
from playwright.sync_api import sync_playwright
html = "<h1>Invoice INV-2026-0142</h1><p>Total ₹49,402.50</p>"
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
page.set_content(html)
page.pdf(path="invoice.pdf", format="A4", print_background=True)
browser.close()An HTML to PDF API from Python
With Templater your code sends data to a saved template and gets the PDF back; there is nothing to install.
import os
import requests
res = requests.post(
"https://templater.nestutils.com/v1/pdfs/render",
params={"wait": 20},
headers={"Authorization": f"Bearer {os.environ['TEMPLATER_KEY']}"},
json={
"template": "gst-invoice",
"returnAs": "binary",
"data": {"invoice": {"number": "INV-2026-0142", "total": 49402.5}},
},
timeout=30,
)
if res.status_code == 202:
print("still rendering:", res.json()["data"]["id"]) # poll GET /v1/pdfs/{id}
else:
res.raise_for_status()
with open("invoice.pdf", "wb") as f:
f.write(res.content)Which one should you use?
WeasyPrint suits reports and documents with lots of print CSS and no JavaScript. Playwright suits pages that must match Chrome exactly.
An API suits you when: You want the same template edited by your team in a browser, previewed on real data, versioned, and rendered without running Chrome yourself. You pay per render instead of per server.
Questions
Is wkhtmltopdf still an option?
It still runs, but the project is no longer maintained and it uses an old WebKit, so modern CSS often breaks. Prefer WeasyPrint or Chrome.
How do I use Django or Jinja templates?
Render the template to an HTML string first (render_to_string in Django), then pass that string to WeasyPrint or Playwright.
Why is the PDF blank or missing images?
Relative image paths have nothing to resolve against. Use absolute URLs, or pass base_url to WeasyPrint’s HTML().