Renderpaper
Log in Start free

HTML to PDF in Python

Python has more HTML-to-PDF options than any other ecosystem, and each one asks for something back. WeasyPrint wants Pango, cairo and GDK-PixBuf installed at the system level — fine on your laptop, an afternoon on Alpine, and a recurring surprise every time a base image moves. pdfkit wants a wkhtmltopdf binary whose rendering engine predates flexbox. xhtml2pdf supports a subset of CSS 2.1. pyppeteer is unmaintained; Playwright works and brings a browser with it.

This is one requests call and no system packages.

One-off render

Only requests, and even that is a convenience — urllib works identically.

import os
import requests

html = """<!doctype html>
<html><head><style>
  @page { size: A4; margin: 20mm; }
  body  { font: 14px/1.6 system-ui, sans-serif; color: #16181d; }
  h1    { font-size: 28px; margin: 0 0 4px; }
</style></head>
<body>
  <h1>Invoice 2026-0142</h1>
  <p>Issued 2026-08-08 · Due 2026-09-07</p>
</body></html>"""

response = requests.post(
    "https://renderpaper.com/v1/render",
    headers={"X-API-Key": os.environ["RENDER_API_KEY"]},
    json={"template": html},
    timeout=60,
)
response.raise_for_status()

with open("invoice.pdf", "wb") as f:
    f.write(response.content)
print("wrote invoice.pdf")
RENDER_API_KEY=rs_live_… python invoice.py

Stored templates: send data, not markup

Keeping the document inside your Python means every visual change is a deploy. Store it once, then post data.

import os
import requests

TEMPLATE_ID = "your-template-id"

response = requests.post(
    f"https://renderpaper.com/v1/templates/{TEMPLATE_ID}/render",
    headers={"X-API-Key": os.environ["RENDER_API_KEY"]},
    json={
        "data": {
            "InvoiceNumber": "2026-0142",
            "Customer": {"Name": "Northwind Trading AB"},
            "LineItems": [
                {"Description": "Consulting", "Quantity": 12, "Amount": 14400},
                {"Description": "Hosting", "Quantity": 1, "Amount": 4350},
            ],
            "Total": 18750,
        }
    },
    timeout=60,
)
response.raise_for_status()

with open("invoice.pdf", "wb") as f:
    f.write(response.content)

The stored template is a Go html/template. The syntax is close enough to Jinja to read without learning it:

<h1>Invoice {{.InvoiceNumber}}</h1>
<p>{{.Customer.Name}}</p>
<table>
  {{range .LineItems}}
  <tr><td>{{.Description}}</td><td>{{.Quantity}}</td><td>{{.Amount}}</td></tr>
  {{end}}
</table>

Streaming without holding the file in memory

For large documents, stream the response rather than buffering it:

import os
import requests

with requests.post(
    "https://renderpaper.com/v1/render",
    headers={"X-API-Key": os.environ["RENDER_API_KEY"]},
    json={"template": "<h1>Statement</h1>"},
    stream=True,
    timeout=120,
) as response:
    response.raise_for_status()
    with open("statement.pdf", "wb") as f:
        for chunk in response.iter_content(chunk_size=64 * 1024):
            f.write(chunk)

Why not WeasyPrint

WeasyPrint is genuinely good and its CSS Paged Media support is better than Chromium's — named pages, @page :first, running elements and margin boxes all work properly, and that is not a small thing for print typography.

The trade is the rest of CSS and the install. Flexbox support arrived late and grid is still not there, so a layout built with modern tools does not survive the trip. And it is not a pure-Python dependency: Pango, cairo and their headers have to exist in every environment you deploy to.

Choose WeasyPrint when the document is print-first and typographic, and you control the environment. Choose a browser renderer when the document is web-first and built with the same CSS as the rest of your product.

Why not pdfkit and wkhtmltopdf

pdfkit is a thin wrapper around the wkhtmltopdf binary, so it inherits that engine's age. It is a fork of a WebKit from before flexbox and grid existed; modern layouts collapse quietly rather than failing loudly, which is the worst way for a document to be wrong. The project has been unmaintained for years.

It is still a reasonable choice for a fixed, simple document that nobody redesigns.

Try it without a key

https://renderpaper.com/sample.pdf is a real render, publicly served. The free tier is 50 documents a month with no card.

Last updated 2026-08-08.