The two Python routes: Jinja2 with WeasyPrint, or an HTTP call
To generate a PDF from a template in Python, fill an HTML template with Jinja2 and convert the result with WeasyPrint; both are open source and the whole job is about five lines. The alternative is to keep the template in a hosted editor and send JSON to a template API with the requests library.
The first route is enough for most Python projects where a developer owns the layout. The second is for teams where someone else does. If you are still choosing between approaches, the overview of the three template routes comes first.
Route 1: Jinja2 and WeasyPrint, step by step
Prerequisites. WeasyPrint's installation page lists Python 3.10 or later and Pango 1.44 or later for version 70.0. Pango is a system library, not a pip package: on Linux it comes from your package manager, and on Windows the documentation installs it through MSYS2. Check this before you plan a deployment.
- Install the renderer:
pip install weasyprint. Install Jinja2 from PyPI as well. - Create the template environment:
env = Environment(loader=FileSystemLoader("templates"), autoescape=select_autoescape()) - Load the template file:
template = env.get_template("invoice.html") - Merge the data:
html = template.render(invoice_number="INV38379", total_amount=82542.56) - Render the PDF:
HTML(string=html).write_pdf("invoice.pdf")
The imports are from jinja2 import Environment, FileSystemLoader, select_autoescape and from weasyprint import HTML. Called with no arguments, write_pdf() returns the PDF as bytes instead of writing a file, which is what you want in a web view. Page size and margins are plain CSS: the documentation's example is CSS(string='@page { size: A3; margin: 1cm }'), passed through the stylesheets argument of write_pdf.
Route 2: a template API over HTTP with requests
Prerequisites. A CraftMyPDF account, a template designed in its editor, the template ID and your API key. The Free plan covers 50 PDFs a month and 3 templates (pricing page, 3 October 2026).
- Build the payload:
payload = {"template_id": "05f77b2b18ad809a", "export_type": "json", "data": data}wheredatais your dictionary. The API reference accepts a JSON object or a stringified one, and its own sample usesjson.dumps(data). - Send it:
response = requests.post("https://api.craftmypdf.com/v1/create", headers={"X-API-KEY": api_key}, json=payload) - Read the link:
pdf_url = response.json()["file"]. The response also carriesstatusandtransaction_ref. - Fetch the PDF from that URL before it expires, or set
"export_type": "file"to receive the PDF bytes directly inresponse.content.
Optional parameters from the reference include expiration (minutes the link stays valid, default 5), output_file (file name, default output.pdf) and version (a specific template version). The CraftMyPDF API guide covers the asynchronous endpoint and regional hosts.
Worked example: one invoice on both routes
Take the invoice data from CraftMyPDF's reference sample: invoice number INV38379, date 2021-09-30, currency USD, total 82542.56.
On route 1, invoice.html contains placeholders such as {{ invoice_number }} and {{ total_amount }}. template.render(...) returns the filled HTML string and WeasyPrint writes invoice.pdf to disk. Nothing leaves your server.
On route 2, the same four values go in the data dictionary. The service returns a JSON body whose file value is a link to the PDF. One call uses one credit, whatever the page count.
| Jinja2 + WeasyPrint | Template API (CraftMyPDF) | |
|---|---|---|
| Layout is edited in | HTML and CSS files | A drag-and-drop editor |
| Runs on | Your server, needs Pango | The vendor's servers |
| Cost | Open source | Free for 50 PDFs a month, then from $29 a month |
| Data leaves your network | No | Yes |
A mistake to avoid on each route
- WeasyPrint: passing the HTML as a positional argument.
HTML('<h1>foo')is read as a file name. A string in memory must be passed by name, asHTML(string=...). The documentation calls this out directly. - WeasyPrint: rendering untrusted HTML. The documentation warns that untrusted HTML or CSS can cause security problems. Keep
autoescapeon in Jinja2 for user-supplied values. - Template API: saving the link instead of the file. The default link lifetime is 5 minutes. Download the PDF or raise
expiration. - Either route: checking only short documents. Test a long table before release; see how page breaks go wrong.
If generation fails in production, the failed generations checklist lists what to log.
Sources used for this page
The facts above come from the pages below, read on 3 October 2026. We have not used these products hands-on. Prices and terms change, so confirm them on the vendor's site before you buy.
- First Steps - WeasyPrint documentation — Vendor documentation · doc.courtbouillon.org · checked 2026-10-03
- API - Jinja documentation — Vendor documentation · jinja.palletsprojects.com · checked 2026-10-03
- CraftMyPDF API Reference (OpenAPI specification) — Vendor documentation · craftmypdf.com · checked 2026-10-03
- Pricing - CraftMyPDF.com — Vendor pricing page · craftmypdf.com · checked 2026-10-03
- Quickstart - Requests documentation — Vendor documentation · requests.readthedocs.io · checked 2026-10-03