§ 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 HTML
Live preview
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 afinallyblock. - 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.
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?
What is the best Python library for PDF generation?
How do I convert HTML to PDF in Python?
Does WeasyPrint support JavaScript?
Is wkhtmltopdf still maintained?
Can PyPDF2 create a PDF from HTML?
WeasyPrint vs ReportLab: which should I use?
How do I generate a PDF from a Django template?
§ Index
More PDF and archiving tools
- Convert HTML to PDF
- Webpage to PDF
- Save webpage as PDF
- URL to PDF API
- Website archiving
- Screenshot API
- React to PDF
- HTML to PDF Node.js
- Laravel HTML to PDF
- Vue to PDF
- Next.js PDF generator
- Angular to PDF
- Markdown to PDF API
- Django HTML to PDF
- Blazor HTML to PDF
- Spring Boot HTML to PDF
- Airtable to PDF
- Rails HTML to PDF
- PDF generator API
- Website archiving software
- Document generation API
- Bulk HTML to PDF
- Wayback Machine alternative
- Best HTML to PDF API
- DocRaptor alternative
- Puppeteer alternative
- Wkhtmltopdf alternative
- PDFShift alternative
- Urlbox alternative
- PDFCrowd alternative
- Api2Pdf alternative
- Browserless alternative
- APITemplate alternative
- CraftMyPDF alternative
- PDFMonkey alternative
- dompdf alternative
- Gotenberg alternative
- GrabzIt alternative
- PDF API pricing
- How it works
- Features
- Pricing
§ 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.