The journal · 16 July 2026 · 9 min read
HTML to PDF Python: the 5 ways that actually work in 2026
Half the Python HTML to PDF advice online points at a library that shells out to an abandoned binary. Here is the current map: what renders what, the code for each, and where each one falls over.
The short version: there are exactly five ways to convert HTML to PDF in Python that are worth your time in 2026. WeasyPrint if your HTML is static and you care about print layout. Playwright if the page needs JavaScript to finish rendering. xhtml2pdf if your needs are genuinely tiny. A rendering API if you do not want to operate any of it. And pdfkit, which most tutorials still recommend, belongs on the list only so we can tell you why it should not be in new code: it is a thin wrapper around wkhtmltopdf, a binary that was archived in January 2023 and carries an unpatched 9.8 severity SSRF vulnerability.
Everything below comes with working code and the specific way each option falls over, because they all fall over somewhere.
What is the best Python library to convert HTML to PDF?
WeasyPrint, for most Python codebases. It is pure Python, actively maintained with regular releases, installable with pip install weasyprint plus a couple of system libraries, and it takes print CSS seriously: @page rules, page breaks that respect page-break-inside: avoid, running headers, page counters. For invoices, reports, statements and anything else built from templates you control, it is the healthiest library in the ecosystem.
from weasyprint import HTML
HTML(string=rendered_template).write_pdf(
"invoice-8412.pdf",
)
Three lines, no browser, no subprocess. Pair it with Jinja2 or Django templates and you have the classic Python document pipeline: render the template to an HTML string, hand the string to WeasyPrint, get bytes back. If you are specifically on Django, the Django HTML to PDF page walks the same choice with framework code and covers when a page's JavaScript charts push you past WeasyPrint.
The failure mode is a hard one, though, and you should know it before you commit.
Does WeasyPrint support JavaScript?
No, and it never will by design. WeasyPrint is a CSS layout engine, not a browser. It parses your HTML and stylesheets and lays out pages itself; it does not execute a single line of JavaScript. If your template is a React page, pulls data client side, or draws charts with Chart.js after a fetch resolves, WeasyPrint renders the empty shell that exists before any of that runs.
It also implements CSS on its own timetable. Most of what you write for documents works: flexbox works, grid support has matured, fonts and floats are fine. But a stylesheet built for Chrome will occasionally hit a property WeasyPrint handles differently, and the fix is adjusting the template, not the library. Keep your PDF templates separate from your application UI and this rarely hurts.
When the page needs a real browser: Playwright
If the content you are converting only exists after JavaScript runs, you need an actual browser engine, and in Python that means Playwright driving headless Chromium. The output is exactly what Chrome would print, because it is Chrome printing.
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
page.goto("https://app.example.com/reports/q2")
page.wait_for_selector("#revenue-chart")
page.pdf(path="q2-report.pdf", format="A4")
browser.close()
The wait_for_selector line is the one that separates working pipelines from bug reports. Charts, tables and anything data driven arrive after page load; render before they land and the PDF has an empty box where the chart should be. We wrote up the full catalogue of these surprises in why your HTML to PDF output does not match the browser.
The cost of Playwright is not the code, it is the operations. You are now running Chromium in production: 100 to 300 MB of memory per instance, more on heavy pages, processes that leak and zombie under load, font packages to install in every container, and a queue in front of it all once volume grows. On a laptop it is delightful. As infrastructure it is a part time job.
How do I convert HTML to PDF in Python without wkhtmltopdf?
This question shows up constantly now, because teams are discovering their dependency the hard way. If your requirements file says pdfkit, you are running wkhtmltopdf: pdfkit is a subprocess wrapper around the binary. The project was archived on GitHub in January 2023, its last release shipped in 2020, and CVE-2022-35583, a server side request forgery vulnerability rated 9.8, will never be patched. Its frozen WebKit engine also predates CSS grid entirely, so modern stylesheets lay out wrong on top of the security problem.
The migration path depends on what your templates need. Static HTML and CSS: WeasyPrint, and most templates port with minor stylesheet adjustments. JavaScript dependent pages: Playwright or a rendering API. We keep a full wkhtmltopdf alternative breakdown current, including what breaks during migration and how to test for it with side by side renders.
The small option: xhtml2pdf
xhtml2pdf is pure Python on top of ReportLab. It installs anywhere, needs no system libraries, and for simple documents, a table, some headings, basic styling, it does the job. Its CSS support is a small subset of the real thing, so anything visually ambitious comes out looking like 2009. That is not an insult; it is a scope statement. For a plain packing slip generated inside a constrained environment, it is a reasonable tool. For anything a designer touched, it is not.
The comparison, in one table
| Option | JavaScript | Print CSS quality | You operate | Use it for |
|---|---|---|---|---|
| WeasyPrint | No | Excellent | A pip dependency | Static templates: invoices, reports |
| Playwright | Full | Good (Chromium print) | A browser fleet | JS heavy pages, live app views |
| pdfkit / wkhtmltopdf | Frozen, ancient | Buggy page breaks | An archived binary with a 9.8 CVE | Nothing new. Migrate. |
| xhtml2pdf | No | Minimal subset | A pip dependency | Very simple documents |
| Rendering API | Full | Good (Chromium print) | Nothing | Production volume without browser ops |
The API option: three lines and no browser to babysit
If the honest answer to "who maintains the rendering fleet" is nobody, the fifth option is to make it an HTTP call. From Python that is just requests:
import requests
resp = requests.post(
"https://api.sitepdf.com/v1/render",
headers={"Authorization": f"Bearer {api_key}"},
data={
"url": "https://app.example.com/invoices/8412",
"wait_for": "#invoice-total",
"format": "A4",
"archive": "true",
},
)
The render happens in managed Chromium, wait_for holds it until your content is on screen, and archive=true stores a timestamped snapshot of the source page alongside the PDF, which matters the day someone asks what the invoice page actually said in March. The PDF generator API page documents the full option set, and you can sanity check how your template renders in Chromium right now with the free in-browser converter, before writing any integration code.
One adjacent note from teams doing this for billing: generating the invoice PDF is usually the easy half of the workflow. Getting it paid is the other half, and there is dedicated software that will chase every unpaid invoice by email and SMS automatically once your documents go out. Worth knowing if invoices are why you are here.
Which one should you pick?
- Your templates are static and you control them: WeasyPrint. It is the best pure Python answer and the print CSS support is genuinely good.
- The page needs JavaScript and you have ops capacity: Playwright, with
wait_for_selectorin every render path and a memory cap on the browser pool. - The page needs JavaScript and you do not want the fleet: a rendering API. One HTTP call, and the browser is someone else's pager.
- You found pdfkit in requirements.txt: schedule the migration. The binary underneath is archived and the SSRF is not getting fixed.
- The document is trivial and the environment is locked down: xhtml2pdf is fine. Keep expectations 2009 shaped.
Whatever you choose, run ten real documents through it before you commit, side by side against how Chrome prints the same HTML. Every engine on this list disagrees with the others somewhere, and it is much cheaper to find out where on day one.
Written by the team building Sitepdf, an HTML to PDF API that archives every page it renders. The in-browser converter is free to try; early access locks the launch pricing.