Practical guide

Generate PDF from template in Python: Jinja2 with WeasyPrint, or a template API with requests

Last materially reviewed 2026-10-03

Quick answerIn Python, render a Jinja2 HTML template and pass it to WeasyPrint's HTML(string=...).write_pdf(). If non-developers must edit the layout, POST JSON to a template API with requests instead.

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.

  1. Install the renderer: pip install weasyprint. Install Jinja2 from PyPI as well.
  2. Create the template environment: env = Environment(loader=FileSystemLoader("templates"), autoescape=select_autoescape())
  3. Load the template file: template = env.get_template("invoice.html")
  4. Merge the data: html = template.render(invoice_number="INV38379", total_amount=82542.56)
  5. 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).

  1. Build the payload: payload = {"template_id": "05f77b2b18ad809a", "export_type": "json", "data": data} where data is your dictionary. The API reference accepts a JSON object or a stringified one, and its own sample uses json.dumps(data).
  2. Send it: response = requests.post("https://api.craftmypdf.com/v1/create", headers={"X-API-KEY": api_key}, json=payload)
  3. Read the link: pdf_url = response.json()["file"]. The response also carries status and transaction_ref.
  4. Fetch the PDF from that URL before it expires, or set "export_type": "file" to receive the PDF bytes directly in response.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 with WeasyPrint vs a template API from Python
Jinja2 + WeasyPrintTemplate API (CraftMyPDF)
Layout is edited inHTML and CSS filesA drag-and-drop editor
Runs onYour server, needs PangoThe vendor's servers
CostOpen sourceFree for 50 PDFs a month, then from $29 a month
Data leaves your networkNoYes

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, as HTML(string=...). The documentation calls this out directly.
  • WeasyPrint: rendering untrusted HTML. The documentation warns that untrusted HTML or CSS can cause security problems. Keep autoescape on 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.

  1. First Steps - WeasyPrint documentation — Vendor documentation · doc.courtbouillon.org · checked 2026-10-03
  2. API - Jinja documentation — Vendor documentation · jinja.palletsprojects.com · checked 2026-10-03
  3. CraftMyPDF API Reference (OpenAPI specification) — Vendor documentation · craftmypdf.com · checked 2026-10-03
  4. Pricing - CraftMyPDF.com — Vendor pricing page · craftmypdf.com · checked 2026-10-03
  5. Quickstart - Requests documentation — Vendor documentation · requests.readthedocs.io · checked 2026-10-03