HTML to PDF
in Python.
Python's PDF story is split between libraries that draw the document and libraries that implement their own slice of CSS. Posting the HTML to something running Chrome sidesteps both.
The call
Post your markup with an API key. The PDF comes back in the same response, base64 encoded by default. Nothing is written to disk on our side.
pip install requestsimport base64
import os
import requests
res = requests.post(
"https://api.html4pdf.com/generate-pdf",
headers={"Authorization": f"Bearer {os.environ['HTML4PDF_KEY']}"},
json={
"html": "<h1>Invoice INV-1042</h1><p>Total: $1,645.40</p>",
"options": {"format": "A4", "printBackground": True},
},
timeout=60,
)
res.raise_for_status()
data = res.json()
with open("invoice.pdf", "wb") as f:
f.write(base64.b64decode(data["pdf"]))
print("saved invoice.pdf,", data["creditsRemaining"], "credits left")Returning it from Django or FastAPI
Ask for a binary response and hand the bytes straight to your framework. Nothing touches the filesystem.
from django.http import HttpResponse
def invoice_pdf(request, number):
res = requests.post(
"https://api.html4pdf.com/generate-pdf",
headers={"Authorization": f"Bearer {settings.HTML4PDF_KEY}"},
json={"html": render_to_string("invoice.html", {"number": number}),
"responseFormat": "binary"},
timeout=60,
)
res.raise_for_status()
return HttpResponse(
res.content,
content_type="application/pdf",
headers={"Content-Disposition": f'attachment; filename="{number}.pdf"'},
)What comes back
The default response is JSON with the document base64 encoded, the credits you have left, and how long the render took. Send responseFormat as binary instead and the body is the PDF itself, with a Content-Type of application/pdf.
{
"success": true,
"pdf": "JVBERi0xLjQKJeLjz9MKMyAwIG9...",
"creditsRemaining": 9,
"metadata": { "size": 42813, "processingTime": 2841 }
}A 402 means you are out of credits, a 401 means the key is wrong, and a 422 means the markup could not be rendered. Everything else is worth retrying once.
The usual Python libraries
Worth knowing before you add a dependency, because for a lot of documents one of these is the right answer and costs nothing.
| Library | What it is | The trade-off |
|---|---|---|
| WeasyPrint | Renders HTML and CSS to PDF in pure Python, with strong CSS Paged Media support. | Excellent for print layout, headers and footers. It does not run JavaScript, so anything drawn by a script is simply absent. |
| pdfkit with wkhtmltopdf | A thin wrapper around the wkhtmltopdf binary. | You install and ship a native binary, and inherit an unmaintained rendering engine that predates modern CSS layout. |
| ReportLab | Builds the PDF programmatically, element by element. | Total control and no HTML at all. Every design change is a code change. |
| Playwright for Python | Drives headless Chromium from Python. | Correct rendering, at the cost of installing browsers and managing them in production. |
Python: common questions
What is the best way to convert HTML to PDF in Python?
If your page is plain HTML and CSS without scripts, WeasyPrint is a good pure-Python answer. If it uses modern CSS or JavaScript, you need a real browser, either by running Playwright yourself or by posting the HTML to an API that already runs one.
Can I use it with Django or Flask?
Yes. Render your template to a string as usual, post that string, and return the bytes with a PDF content type. The Django example above is the whole pattern.
Does it support JavaScript charts?
Yes. Rendering happens in headless Chrome, so Chart.js and similar libraries draw before the page is captured.
Is WeasyPrint not enough?
Often it is, and it is free. The reasons to leave it are JavaScript, flexbox and grid edge cases, and not wanting system libraries like Pango and cairo in your container.
Do you store the HTML I send?
No. It is rendered in memory and released with the response.