TL;DR. Choose WeasyPrint when your HTML is server-rendered, has no JavaScript, and you can install Pango and Cairo. It is the lightest dependency footprint and produces the smallest PDF files (8 KB simple, 21 KB complex). Choose playwright-python when templates rely on JavaScript, modern CSS Grid, or you need warm-pool latency under 10 ms per render. Playwright ships ~300 MB of Chromium and renders 15-75x faster than WeasyPrint in warm mode, but is harder to deploy serverless.
This guide compares the two libraries on the dimensions that decide most Python projects in 2026: performance, JavaScript and CSS coverage, deployment story, and which framework (Django, Flask, FastAPI) maps to which choice. All numbers come from the HTML to PDF benchmark 2026, reproducible from the public fixtures.
Quick comparison
| Factor | WeasyPrint 68 | playwright-python |
|---|---|---|
| Engine | Pure Python (Pango + Cairo) | Chromium (CDP over WebSocket) |
| JavaScript support | None | Full (V8) |
| CSS Grid / Flexbox | Partial Grid, full Flexbox | Full (Chromium) |
| Cold render (simple doc) | 227 ms | 42 ms |
| Cold render (complex doc) | 629 ms | 119 ms |
| Warm render | Not applicable (no warm mode) | 3 ms simple, 13 ms complex |
| File size (simple) | 8 KB | 16 KB |
| File size (complex) | 21 KB | 59-125 KB |
| Install footprint | ~30-50 MB (Python + Pango + Cairo) | ~300 MB (Chromium binary) |
| Serverless friendliness | Hard (system libs) | Hard (binary size) |
| Maintained by | WeasyPrint contributors (Kozea) | Microsoft |
The 30-second decision
Three questions answer 90% of cases:
- Does the source HTML rely on JavaScript (charts, dynamic tables, client-side framework rendering)? If yes, you need playwright-python. WeasyPrint is out. Stop here.
- Are you deploying to AWS Lambda or Vercel without a custom layer? If yes, neither library is a clean fit; consider a managed API.
- Do you generate more than ~10 PDFs per second sustained? If yes, only Playwright with a warm browser pool keeps up. WeasyPrint's 227 ms floor saturates a single CPU at 4-5 PDFs/second.
If JavaScript is not in your templates, your stack tolerates Cairo system dependencies, and your throughput is moderate, WeasyPrint is the simpler answer. Smaller install, smaller PDFs, no headless browser to babysit.
Performance: real numbers
These numbers come from the full HTML to PDF benchmark, measured on macOS arm64 with Node 22 and Python 3.12, median of 5 runs.
| Tool | Document | Mode | Latency | File size |
|---|---|---|---|---|
| WeasyPrint 68 | Simple | Cold only | 227 ms | 8 KB |
| WeasyPrint 68 | Complex (50-row invoice) | Cold only | 629 ms | 21 KB |
| Playwright 1.58 | Simple | Cold | 42 ms | 16 KB |
| Playwright 1.58 | Complex | Cold | 119 ms | 59 KB |
| Playwright 1.58 | Simple | Warm | 3 ms | 16 KB |
| Playwright 1.58 | Complex | Warm | 13 ms | 125 KB |
The two numbers that matter for a production decision:
- Warm Playwright is 75x faster than WeasyPrint on simple documents (3 ms vs 227 ms) and 48x faster on complex ones (13 ms vs 629 ms).
- WeasyPrint produces 50-80% smaller PDFs because it generates native PDF primitives instead of routing through a print engine that embeds extra resources.
WeasyPrint has no warm mode. Each call to HTML(string=html).write_pdf() spawns a fresh Python interpreter cycle through the Pango and Cairo bindings; there is no equivalent of Playwright's persistent browser process. This is a fundamental architectural choice, not a missing feature.
Architecture: why they behave differently
WeasyPrint is a pure Python implementation of CSS 2.1 paged media plus parts of CSS Level 3. It parses HTML, builds a render tree, applies CSS, calls Pango for text shaping, and emits PDF via Cairo. There is no browser, no V8, no DOM beyond what CSS needs. The trade-off is straightforward: you get exactly what HTML and CSS describe, nothing more.
playwright-python drives a Chromium process over the Chrome DevTools Protocol. Your HTML is loaded as a real web page, JavaScript runs, the layout engine handles CSS, and Chromium's print-to-PDF code path produces the file. You get everything Chromium does, plus Chromium's resource footprint.
The performance gap follows from this. Chromium is heavily optimized for incremental layout and reuses its V8 isolates and resource caches across page renders inside one browser. That is why a warm Playwright render costs 3 ms: most of the work was paid once at browser launch. WeasyPrint cannot amortize anything across renders because each call is a self-contained process spin-up.
Code: side-by-side Python examples
The minimal "render this HTML to a PDF buffer" looks like this in each library.
from weasyprint import HTML
def render_pdf(html: str) -> bytes:
return HTML(string=html).write_pdf()The cold Playwright pattern is shorter but pays the 42-119 ms launch cost on every call. The warm pool pattern adds ~10 lines of state and gets you to 3-13 ms. WeasyPrint is always one line because there is no browser to keep alive.
For async stacks (FastAPI, Starlette, aiohttp), playwright.async_api exposes the same shape with await. WeasyPrint has no async API; wrap calls in asyncio.to_thread() to avoid blocking the event loop.
JavaScript support: usually the dealbreaker
This is the cleanest split between the two libraries.
WeasyPrint does not execute any JavaScript. Not partially, not with a flag. Your templates render whatever the server emits. If a Chart.js chart depends on <script> to draw itself, you get an empty <canvas> element in the PDF. If a React app hydrates client-side, you get the unhydrated SSR shell or, worse, an empty <div id="root">. Same for D3, Vega, Plotly, Mermaid, KaTeX rendered client-side, or any "render after DOM ready" pattern.
playwright-python runs the full V8 engine. Charts draw, components hydrate, you can await page.wait_for_selector() to wait for late-loading content before calling page.pdf(). This is the difference between "static HTML to PDF" and "anything a browser would render to PDF".
Server-side workarounds for WeasyPrint when you need a chart:
- Render the chart server-side as inline SVG (Vega-Lite, matplotlib, plotly with
to_image()). WeasyPrint embeds SVG cleanly. - Pre-render the chart to a PNG via a separate service and embed it as an
<img>.
If you cannot or do not want to render charts server-side, that decision is made: use playwright-python.
CSS coverage: where each tool stops
Both render the common subset (colors, typography, basic layout, tables, page breaks) correctly. The differences appear at the edges.
| Feature | WeasyPrint 68 | Playwright (Chromium) |
|---|---|---|
@page, page-break-*, widows/orphans | Full | Full |
| Flexbox | Full | Full |
| CSS Grid (basic) | Mostly | Full |
subgrid, grid-template-areas advanced | Partial | Full |
| Container Queries | Partial (68+) | Full |
position: sticky in print | No | Limited (print is paginated) |
transform: rotate/scale | Full | Full |
CSS variables (--foo) | Full | Full |
@font-face (Google Fonts) | Yes (with system Pango fonts cached) | Yes (download via wait_until: networkidle) |
| OpenType ligatures, kerning | Full (via Pango) | Full (via Chromium font stack) |
| SVG embedding | Full | Full |
| MathML | Limited | Limited |
For the typical invoice, receipt, contract, or letter-style document, both render identically once you account for fonts. Differences surface in heavily designed PDFs that exploit recent CSS additions.
For a hands-on tour of CSS features that matter in print, see the CSS print styles guide.
Production: what breaks for each
WeasyPrint pain points
- System dependencies: Pango, Cairo, and GDK-PixBuf need to be installed at the OS level. On Debian:
apt install libpango-1.0-0 libpangoft2-1.0-0 libcairo2 libgdk-pixbuf-2.0-0. On Alpine: more involved because of musl. On Windows, the official installer bundles them. - Fonts: WeasyPrint uses fonts installed in the Pango fontconfig cache. If your container does not include the typeface you want, text falls back to a default sans. Add
apt install fonts-noto fonts-liberationor mount custom font files and rebuild the cache. - Throughput ceiling: 227 ms cold floor means ~4 PDFs/second on one CPU, fully utilized. Beyond that, you scale horizontally (more processes) or move to Playwright with a warm pool.
- Limited error reporting: malformed CSS often renders silently with the offending rule dropped. Debug with
WeasyPrint's logging at DEBUG level.
playwright-python pain points
- Binary size:
playwright install chromiumdownloads ~280 MB. Multiplied across CI runs and Docker layers, this matters. - Memory: ~150 MB resident per browser instance, plus ~30 MB per active page. A warm pool of 4 browsers and 8 pages each is ~750 MB before your app code.
- Crashes under load: headless Chromium will OOM under memory pressure. You need restart logic in your pool.
- Serverless deploys: AWS Lambda's 250 MB unzipped layer limit excludes a vanilla Chromium. Workarounds (
@sparticuz/chromium-minplus a Brotli-compressed binary, or container Lambda) add operational surface. - Page leaks: forgetting
page.close()in an exception path leaks memory until the browser process is recycled.
Both libraries are runnable in production. Neither is a one-line ops story.
Choosing for Django, Flask, FastAPI
| Stack | Workload | Recommendation |
|---|---|---|
| Django app, server-rendered invoices/letters, no JS in templates | Low-medium volume | WeasyPrint via WeasyPrint package or django-weasyprint |
| Flask app, charts in PDFs (Plotly/Chart.js) | Any volume | playwright-python with a warm pool |
| FastAPI async app, dashboards-to-PDF | Medium-high volume | playwright-python async API with warm pool |
| Any Python stack on AWS Lambda or Vercel | Any volume | Managed API (PDF4.dev) avoids the binary deploy problem |
| Any Python stack, locked-down container, no Cairo, no Chromium | Low volume | xhtml2pdf or ReportLab (covered in the Python guide) |
| Migrating from pyppeteer | Any | playwright-python (pyppeteer is unmaintained since 2022) |
Default for a fresh Python project with server-rendered HTML and no JavaScript: WeasyPrint. The smaller install, smaller PDFs, and absence of a browser process to manage make it the path of least operational resistance, as long as your throughput stays under ~4 PDFs/second.
For a complete walkthrough including Django and FastAPI examples for both libraries, see Generate PDFs from HTML in Python.
WeasyPrint in 2026: state of the project
WeasyPrint 68 (early 2026) is the current major release. The project is actively maintained by Kozea with releases roughly every 2-3 months. Recent additions worth noting:
- CSS Grid coverage improved significantly in v66-68. Most realistic Grid layouts now render correctly;
subgridand complexgrid-template-areasare still partial. - Container Queries landed partially in v68.
- Font handling uses Pango 1.50+ with HarfBuzz for shaping, giving correct rendering for non-Latin scripts (Arabic, Devanagari, CJK).
- PDF/A output is supported via
write_pdf(variant='pdf-a-3b')for archival use cases.
WeasyPrint is not catching up to Chromium on JavaScript or modern web platform features. The roadmap explicitly stays inside CSS-only paged media. If your templates are CSS-only HTML, this is a feature, not a limitation.
Skip the choice: managed API
If neither library is appealing because you do not want to ship Chromium and you do not want to install Pango on every container, a managed API trades infrastructure for a per-render cost.
import requests
def render_pdf(html: str) -> bytes:
r = requests.post(
"https://pdf4.dev/api/v1/render",
headers={"Authorization": "Bearer p4_live_xxx"},
json={"html": html, "format": {"preset": "a4"}},
)
r.raise_for_status()
return r.contentPDF4.dev runs warm Playwright (Chromium) pools so you get the 3-13 ms warm-render path on every request without managing browsers, Cairo, or Lambda layers. The same format and Handlebars-style variables work for both invoices and dashboards. Try it free, no credit card.
The trade-off is the standard one: latency includes a network round-trip (typically 50-150 ms from a US/EU server), and you depend on an external service. In return, you ship none of Chromium, none of Pango, no warm-pool code, and no crash recovery.
Summary
- JavaScript in your HTML? playwright-python. WeasyPrint cannot run it.
- No JavaScript, low-medium volume, OK with Cairo system deps? WeasyPrint. Lighter, smaller PDFs, simpler ops.
- High volume (>10 PDFs/sec) without a custom pool? Managed API.
- Serverless (Lambda, Vercel)? Managed API. Both libraries fight the platform.
- Existing pyppeteer code? Migrate to playwright-python; pyppeteer is unmaintained.
For more, the parent benchmark covers Node.js numbers and wkhtmltopdf in the HTML to PDF benchmark 2026, and the Node-side comparison is in Playwright vs Puppeteer for PDF generation. To test a real PDF render in the browser without writing any code, the free HTML to PDF tool uses the same Playwright pipeline.
Free tools mentioned:
Start generating PDFs
Build PDF templates with a visual editor. Render them via API from any language in ~300ms.


