Skip to content

§ Python PDF generation

Python PDF Generation: Python PDF Library Choices and HTML to PDF Python, Compared

Six real ways to produce a PDF from Python, and the honest tradeoff in each. WeasyPrint is the best pure Python HTML renderer until your template needs JavaScript. pdfkit still wraps a binary that was archived in January 2023. ReportLab draws beautifully but never touches HTML. Here is which one fits your document, and when a rendering API is the cheaper answer. Try it on any URL below.

  • Early access, launching soon
  • No card required
  • Your HTML stays yours

§ Live demo

Runs in your browser. Nothing is uploaded.

Your PDF is downloading. Want this as one API call, with the page archived too?

§ Short answer

For Python PDF generation the right library depends entirely on where your document lives. If it is an HTML template, WeasyPrint is the strongest pure Python option: it installs with pip, supports flexbox, grid and CSS custom properties, and needs no browser. Its hard limit is that it runs no JavaScript at all, so Chart.js and Plotly render as empty boxes. If your document is built from data rather than from a page, ReportLab or fpdf2 are smaller and faster, because you draw the layout yourself. pypdf and pdfminer.six generate nothing at all: they read, merge and extract from PDFs that already exist. Playwright for Python gives you true Chromium fidelity at the cost of operating a browser, and a hosted rendering API gives you the same Chromium output as one HTTPS call with nothing installed. The deciding question is whether your template needs a real browser engine.

Last updated 3 August 2026. Written and fact checked by the Sitepdf team.

§ 00

The six Python PDF routes, side by side

Every Python project lands on one of these. They differ in what they can render, what you have to install, and whether they survive a serverless deploy. Engine and support facts below come from each project own documentation, read on 3 August 2026.

Approach Renders real HTML and CSS Runs JavaScript What you install and operate Best for
WeasyPrint Yes, its own engine No A pip install, plus Pango and system font libraries HTML and CSS reports and invoices with no JavaScript in the template
pdfkit plus wkhtmltopdf Yes, an old WebKit fork Partially, an aged engine A pip install plus a binary whose repository was archived in January 2023 Legacy projects already running it and not yet ready to move
ReportLab or fpdf2 No, you draw the document Not applicable A pure Python dependency, nothing else Invoices and reports built from database rows, not from a page
pypdf or pdfminer.six No, they read existing PDFs Not applicable A pure Python dependency Merging, splitting and extracting text, never generating from HTML
Playwright for Python Yes, full Chromium Yes Browser binaries, plus browser lifecycle and memory tuning Teams who need Chromium fidelity and accept running a browser
Hosted rendering API
Sitepdf
Yes, managed Chromium Yes Nothing, it is an HTTPS call Modern CSS and JavaScript charts at volume, plus a dated record

The honest split: if your PDF is a data driven invoice and your team is comfortable laying it out in code, ReportLab or fpdf2 is a smaller, faster and cheaper answer than anything else here and you should use it. If your document is an HTML template with no JavaScript, WeasyPrint is excellent and free. You only need Chromium when the template runs scripts or uses CSS that WeasyPrint has not implemented, and at that point the question is who runs the browser.

§ 01

WeasyPrint is the best pure Python HTML renderer, and here is exactly where it stops

If you want to turn an HTML template into a PDF without operating a browser, WeasyPrint is the strongest option in the Python ecosystem. It installs with pip, it is actively maintained, and its CSS coverage is far better than most developers expect: flexbox, grid, custom properties, paged media, and Selectors Level 3 and 4 are all supported. For an invoice, a statement or a report template, the output is genuinely good.

from weasyprint import HTML

HTML(string=rendered_template).write_pdf('invoice-8842.pdf')
HTML(url='https://app.example.com/invoices/8842').write_pdf('invoice.pdf')

What matters is understanding why it is fast and small, because the same reason sets its ceiling. WeasyPrint is not a browser. Its own README says it plainly: "It is based on various libraries but not on a full rendering engine like WebKit or Gecko." The project documentation is equally direct about the consequence, explaining that in WeasyPrint "there is no user-interaction, no JavaScript, no live rendering (the document doesn't changed after it was first parsed)".

That single sentence decides most WeasyPrint migrations. If your report renders its charts with Chart.js, Plotly, ApexCharts or D3, those charts arrive as empty containers, because the scripts that would have drawn them never run. The same applies to anything hydrated by React or Vue after page load, and to any content fetched by a client side request. WeasyPrint sees the HTML as it was served, and nothing more.

There are smaller gaps worth knowing before you commit. The CSS support table lists box-shadow as unsupported and limits transforms to 2D, so 3D transformations do not render. Interactive pseudo-classes behave in a way that surprises people: the documentation notes that :hover, :active, :focus, :target and :visited are "accepted as valid but never match anything", which is reasonable for a static document but means styles you attached to them silently disappear. There is also a real system dependency: WeasyPrint needs Pango and system font libraries present, which is a non-event on a normal Linux image and a genuine annoyance on Windows and on some minimal containers.

None of that makes WeasyPrint a poor choice. It makes it a precise one. Server rendered HTML with no scripts and mainstream CSS, and it is the best free answer available. Anything else, and you need a browser engine.

§ 02

pdfkit, wkhtmltopdf and the archived binary problem

Search for Python HTML to PDF and a large share of the results still recommend pdfkit. It is worth being clear about what that package is, because the name misleads two different ways: the Python pdfkit is not the Node library called PDFKit, and it is not a renderer at all. It is a thin wrapper that shells out to the wkhtmltopdf command line binary.

import pdfkit

pdfkit.from_url('https://app.example.com/invoices/8842', 'invoice.pdf')
pdfkit.from_string(rendered_template, 'invoice.pdf')

That works, and for years it was the default answer. The problem is upstream. The wkhtmltopdf repository on GitHub carries the notice "This repository was archived by the owner on Jan 2, 2023. It is now read-only." The binary that does the actual rendering is no longer developed, which means the browser engine underneath it keeps aging while the CSS your designers write keeps moving. Modern grid layouts, newer color syntax and recent font features are the things that break first, and there is no future release coming to fix them. Nor are there upstream security patches for a component whose whole job is to load untrusted web content.

If you have a working wkhtmltopdf pipeline, nothing broke today and you do not need to panic. But it should not be the choice for anything new, and treating it as your long term rendering strategy is planning around a component that stopped being maintained more than three years ago. When you do move, WeasyPrint is the natural pure Python destination if you have no JavaScript, and Chromium is the destination if you do. The wkhtmltopdf alternative comparison goes through the migration in detail.

§ 03

ReportLab and fpdf2: the right answer when you are not rendering a page

There is a whole family of Python PDF libraries that never touch HTML. ReportLab is the long established one, with an imperative canvas API and a higher level Platypus layer for flowing document elements. fpdf2 is the lighter, friendlier option that covers most of the same ground. Both are pure Python, both are small, both run anywhere Python runs including the tightest serverless function, and both emit real vector text.

from reportlab.lib.pagesizes import LETTER
from reportlab.pdfgen import canvas

c = canvas.Canvas('invoice.pdf', pagesize=LETTER)
c.setFont('Helvetica-Bold', 20)
c.drawString(72, 720, 'Invoice 8842')
c.setFont('Helvetica', 10)
c.drawString(72, 700, 'Due 15 September 2026')
c.showPage()
c.save()

The tradeoff is that you are writing the layout engine by hand. If your document is a fixed invoice generated from a database row, that is often preferable rather than merely acceptable: no browser, no CSS surprises, no font pipeline, and milliseconds per document. Teams generating tens of thousands of identical statements a night usually land here for exactly that reason.

It stops being the right call the moment the document is a design artifact. If your invoice template is maintained by designers in HTML and CSS, rebuilding it in drawing calls turns every visual change into an engineering ticket, and keeping the emailed HTML version and the PDF version looking alike becomes a permanent chore. Pick by where your document lives and who owns it, not by which library benchmarks fastest.

§ 04

pypdf, PyPDF2 and pdfminer.six do not generate PDFs from HTML

This deserves its own section because the search volume around these names is enormous and the intent behind it is frequently mismatched. pypdf (the maintained successor to PyPDF2, which is deprecated and should not be used in new code) and pdfminer.six are not generators. They operate on PDFs that already exist.

  • pypdf merges, splits, rotates, crops, encrypts and stamps existing PDF files, and can pull text out of them.
  • pdfminer.six is a text and layout extraction tool, built to get content back out of a PDF with position information intact.
  • pdfplumber, built on pdfminer.six, is what most people actually want for pulling tables out of a PDF.

If you arrived at PyPDF2 looking for a way to turn an HTML invoice into a PDF, it cannot do that, and no amount of configuration will change it. What these libraries are genuinely good at is the step after generation: you render each statement, then use pypdf to merge the batch into one file, or to stamp a page number and a confidentiality footer across every page. That pairing, a renderer for creation and pypdf for assembly, is a very common and very sensible Python setup.

Extraction is a different problem again, and a harder one than it looks when the source is a scan rather than a born digital file. Once a PDF contains only pixels, layout aware document data extraction is the tool for the job, not a text parser.

§ 05

Playwright for Python: real Chromium, in your own infrastructure

When the template genuinely needs a browser, Python has a first class option. Playwright ships an official Python binding, so you can drive headless Chromium directly and get output identical to printing from Chrome: flexbox and grid exactly as designed, web fonts, SVG, and JavaScript rendered charts that actually appear.

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/invoices/8842', wait_until='networkidle')
    page.pdf(path='invoice.pdf', format='Letter', print_background=True)
    browser.close()

The cost is operational, and it is larger than a tutorial suggests. playwright install downloads full browser builds, which is a few hundred megabytes in your image. Each launch spawns a real browser process, so cold starts add roughly 300 to 800ms before anything is painted, and under concurrency you are managing a pool rather than calling a function. Two failure modes bite in production: print_background defaults to false, so colored table headers and background panels silently vanish unless you set it, and if anything raises between launch and close the Chromium process leaks, holding memory until the container dies. Use the context manager or a try/finally every time.

Serverless is where this approach usually stops. AWS Lambda caps the unzipped contents of a deployment package, including layers, at 250 MB, with a 50 MB limit on the zipped upload. A standard Chromium build does not fit, which is why the workaround ecosystem of stripped down Chromium layers exists at all. Container images raise the ceiling to 10 GB but change your whole deployment model. Memory is the next wall: Lambda scales CPU with memory and a full vCPU only arrives at 1,769 MB, so an underprovisioned function renders slowly and an adequately provisioned one is expensive to keep warm. Every one of those problems is caused by the browser living inside your function.

§ 06

Django and Flask: the same choice, one framework layer up

Framework specific tutorials make this look like a separate decision, and it is not. In Django you render a template to a string and hand it to whichever engine you picked; in Flask it is the same two lines with render_template. The engine question is unchanged.

from django.template.loader import render_to_string
from django.http import HttpResponse
from weasyprint import HTML

def invoice_pdf(request, pk):
    html = render_to_string('invoices/detail.html', {'invoice': get_invoice(pk)})
    pdf = HTML(string=html, base_url=request.build_absolute_uri()).write_pdf()
    return HttpResponse(pdf, content_type='application/pdf')

The one Django specific detail worth catching early is base_url. Without it, relative paths to your stylesheets, images and fonts do not resolve, and you get an unstyled document with broken images rather than an error. Passing request.build_absolute_uri() fixes it. If you render through an API instead, the equivalent step is making sure your static assets are reachable from outside your network, or posting the fully inlined HTML.

There is one real architectural difference at the framework layer: rendering in process ties up a worker. A Gunicorn worker spending three seconds on a PDF is three seconds it is not serving requests, so anything beyond occasional use belongs in Celery or an equivalent queue regardless of which engine you chose. The Django HTML to PDF page covers the template specifics, and the document generation API page covers the queue and template merge pattern.

§ 07

The API path: Chromium fidelity, nothing to install

A hosted rendering API runs managed Chromium on its own infrastructure. From Python it is one requests call, so there is no binary to install, no Pango dependency to satisfy, no browser pool to tune, no cold browser launch, and no 250 MB limit to fight. It runs identically on Lambda, Cloud Run, a Docker container or your laptop, because from your application it is an ordinary outbound HTTPS request.

import requests, os

res = requests.post(
    'https://api.sitepdf.com/v1/render',
    headers={'Authorization': f'Bearer {os.environ["SITEPDF_KEY"]}'},
    json={
        'url': 'https://app.example.com/invoices/8842',
        'format': 'Letter',
        'margin': 'normal',
        'archive': True,
    },
)

pdf_url = res.json()['pdf_url']

Send a url and Chromium loads the page the way a visitor would, JavaScript included, so the chart that WeasyPrint left as an empty box renders normally. Post an html string instead if your markup is generated in memory and never served. Output is real vector text, so it stays selectable, searchable and accessible.

The archive flag is the part no local package offers. It stores a timestamped snapshot of exactly what Chromium rendered, retrievable later by id, so months afterwards you can show what a given invoice or statement looked like at the moment you produced it. If you work in finance, insurance or anything with a records requirement, that dated record is usually the reason to render through an API rather than in process. See the PDF generator API page for the full call surface, website archiving for how the record works, and PDF API pricing for what every vendor in this category charges per 1,000 documents.

The honest limit: an API is a network call, so it adds latency an in process library does not have, and it will not work in an air gapped environment. If you generate ten documents a week from a script with no JavaScript in it, WeasyPrint is free and finishes in milliseconds, and you should use it. The case for an API strengthens with volume, with serverless, with JavaScript in the template, and with any requirement to prove what you rendered.

§ 08

Choosing, in one pass

Work down this list and stop at the first line that matches you.

  • You are trying to read, merge or split PDFs that already exist. Use pypdf, or pdfplumber if you need tables out. None of the rest of this page applies to you.
  • The PDF is built from database rows and no HTML template exists. Use ReportLab or fpdf2. Smallest, fastest, cheapest, and it deploys anywhere.
  • The document is an HTML template with no JavaScript, and you can install system libraries. Use WeasyPrint. It is the best free answer in Python and it is actively maintained.
  • You are on wkhtmltopdf or pdfkit today. Nothing is on fire, but plan the move: that binary has been archived since January 2023 and will not gain support for newer CSS.
  • The template runs JavaScript, or uses CSS WeasyPrint has not implemented, and a browser is welcome in your infrastructure. Use Playwright for Python, set print_background=True, and close the browser in a finally block.
  • You need Chromium output but you are on serverless, or rendering at volume, or you need a dated record of what was produced. Use a rendering API. Every constraint in the Playwright section above comes from the browser living inside your own function.
From Python: render a URL in managed Chromium, JavaScript charts included, and keep a dated copy of exactly what was produced.
curl https://api.sitepdf.com/v1/render \
  -H "Authorization: Bearer $SITEPDF_KEY" \
  -F url=https://app.example.com/invoices/8842 \
  -F format=Letter \
  -F margin=normal \
  -F archive=true

{
  "pdf_url": "https://api.sitepdf.com/v1/documents/doc_7m2vc.pdf",
  "pages": 3,
  "rendered_in_ms": 1820,
  "archive": {
    "id": "arc_q47ne",
    "captured_at": "2026-08-03T10:22:41Z",
    "retrieve_url": "https://api.sitepdf.com/v1/archives/arc_q47ne"
  }
}

The API is in early access; this is the documented call shape it opens with. Full request and response walkthrough.

§ 09

Questions about this job

How do I generate a PDF in Python?
Pick by where your document lives. If it is an HTML template, WeasyPrint converts it with two lines and no browser. If it is built from data, ReportLab or fpdf2 let you draw it directly. If the template runs JavaScript, you need a Chromium renderer, either Playwright for Python locally or a hosted rendering API over HTTPS.
What is the best Python library for PDF generation?
There is no single best one, because they solve different problems. WeasyPrint is the best pure Python HTML to PDF renderer. ReportLab and fpdf2 are best when the document is built from data rather than a page. pypdf is best for editing PDFs that already exist. For HTML that runs JavaScript, no pure Python library works and you need Chromium.
How do I convert HTML to PDF in Python?
The shortest route is WeasyPrint: <code>HTML(string=my_html).write_pdf('out.pdf')</code>, or pass <code>url=</code> to fetch a page. It needs Pango and system font libraries installed. If your HTML depends on JavaScript to render charts or hydrate content, WeasyPrint will not run it, and you need Playwright or a rendering API instead.
Does WeasyPrint support JavaScript?
No. WeasyPrint documentation states directly that there is no user interaction, no JavaScript and no live rendering, because the document does not change after it is first parsed. It is not built on a full rendering engine like WebKit or Gecko. Charts drawn by Chart.js, Plotly or D3 will render as empty containers.
Is wkhtmltopdf still maintained?
No. The wkhtmltopdf GitHub repository states that it was archived by the owner on 2 January 2023 and is now read-only. Existing installs keep working, but the WebKit fork underneath will not gain support for newer CSS and receives no upstream security patches. Python pdfkit is a wrapper around that binary, so it inherits the same status.
Can PyPDF2 create a PDF from HTML?
No. PyPDF2, and pypdf which replaces it, work only on PDFs that already exist. They merge, split, rotate, encrypt and extract text. Neither has a rendering engine, so neither can turn HTML into a PDF. Generate the file with WeasyPrint, ReportLab or a rendering API first, then use pypdf to assemble or stamp the result.
WeasyPrint vs ReportLab: which should I use?
Use WeasyPrint when the document already exists as an HTML template and designers maintain it, since you get CSS layout for free. Use ReportLab when the document is generated from database rows and precision matters more than styling, since drawing it directly is faster and avoids a font and CSS pipeline entirely. Many teams use both.
How do I generate a PDF from a Django template?
Render the template to a string with <code>render_to_string</code>, then pass it to your PDF engine and return it as an <code>HttpResponse</code> with content type <code>application/pdf</code>. With WeasyPrint, set <code>base_url=request.build_absolute_uri()</code> or your CSS, images and fonts will not resolve. Move anything beyond occasional use into a Celery task.

§ Early access

Get on the early-access list

The API opens to the list first, in order. Early access locks the planned launch rates for 12 months. No card required, launching soon.

Render + archive, one API