Get started

Playwright vs WeasyPrint: PDF generation in Python (2026 comparison)

Playwright vs WeasyPrint for Python PDF generation: real performance numbers, CSS coverage, JavaScript support, and how to pick for Django, Flask, or FastAPI in 2026.

benoitded12 min read

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

FactorWeasyPrint 68playwright-python
EnginePure Python (Pango + Cairo)Chromium (CDP over WebSocket)
JavaScript supportNoneFull (V8)
CSS Grid / FlexboxPartial Grid, full FlexboxFull (Chromium)
Cold render (simple doc)227 ms42 ms
Cold render (complex doc)629 ms119 ms
Warm renderNot applicable (no warm mode)3 ms simple, 13 ms complex
File size (simple)8 KB16 KB
File size (complex)21 KB59-125 KB
Install footprint~30-50 MB (Python + Pango + Cairo)~300 MB (Chromium binary)
Serverless friendlinessHard (system libs)Hard (binary size)
Maintained byWeasyPrint contributors (Kozea)Microsoft

The 30-second decision

Three questions answer 90% of cases:

  1. 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.
  2. Are you deploying to AWS Lambda or Vercel without a custom layer? If yes, neither library is a clean fit; consider a managed API.
  3. 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.

ToolDocumentModeLatencyFile size
WeasyPrint 68SimpleCold only227 ms8 KB
WeasyPrint 68Complex (50-row invoice)Cold only629 ms21 KB
Playwright 1.58SimpleCold42 ms16 KB
Playwright 1.58ComplexCold119 ms59 KB
Playwright 1.58SimpleWarm3 ms16 KB
Playwright 1.58ComplexWarm13 ms125 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.

FeatureWeasyPrint 68Playwright (Chromium)
@page, page-break-*, widows/orphansFullFull
FlexboxFullFull
CSS Grid (basic)MostlyFull
subgrid, grid-template-areas advancedPartialFull
Container QueriesPartial (68+)Full
position: sticky in printNoLimited (print is paginated)
transform: rotate/scaleFullFull
CSS variables (--foo)FullFull
@font-face (Google Fonts)Yes (with system Pango fonts cached)Yes (download via wait_until: networkidle)
OpenType ligatures, kerningFull (via Pango)Full (via Chromium font stack)
SVG embeddingFullFull
MathMLLimitedLimited

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-liberation or 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 chromium downloads ~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-min plus 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

StackWorkloadRecommendation
Django app, server-rendered invoices/letters, no JS in templatesLow-medium volumeWeasyPrint via WeasyPrint package or django-weasyprint
Flask app, charts in PDFs (Plotly/Chart.js)Any volumeplaywright-python with a warm pool
FastAPI async app, dashboards-to-PDFMedium-high volumeplaywright-python async API with warm pool
Any Python stack on AWS Lambda or VercelAny volumeManaged API (PDF4.dev) avoids the binary deploy problem
Any Python stack, locked-down container, no Cairo, no ChromiumLow volumexhtml2pdf or ReportLab (covered in the Python guide)
Migrating from pyppeteerAnyplaywright-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; subgrid and complex grid-template-areas are 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.content

PDF4.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:

Html To PdfTry it free

Start generating PDFs

Build PDF templates with a visual editor. Render them via API from any language in ~300ms.