Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.
The shortest reliable answer: use WeasyPrint when you control the HTML and want a direct HTML/CSS-to-PDF API; use Playwright when the document depends on browser navigation, JavaScript, or Chromium rendering. Both are documented Python workflows, but they have different installation and deployment requirements.
Choose the rendering approach first
HTML-to-PDF conversion is not one standardized operation. A direct renderer parses your markup and styles, while a browser loads a page and prints it. Your choice should follow the document, not a supposed universal speed or fidelity ranking: the available documentation does not establish a controlled comparison for representative workloads.
| Requirement | Better starting point | Why |
|---|---|---|
| Generated reports with controlled HTML and CSS | WeasyPrint | A compact HTML(...).write_pdf(...) API and no browser download. |
| Pages that navigate, execute JavaScript, or rely on browser behavior | Playwright | Creates a real Chromium page and calls page.pdf(). |
| Production deployment | Either, after a document test suite | WeasyPrint needs native text/layout libraries; Playwright needs browser binaries. |
For either path, test representative pages for fonts, images, links, page breaks, headers and footers, and any PDF conformance requirement before committing to an engine.
WeasyPrint: direct HTML and CSS rendering
WeasyPrint’s documented quickstart constructs an HTML object and writes a PDF. The current documentation identifies version 70.0 and lists Python 3.10 or newer and Pango 1.44 or newer among the requirements. Platform-specific native packages still apply, so follow the installation instructions for your operating system.
#1 Best Overall
Install the package and native prerequisites
- Install the native libraries listed for your operating system in the WeasyPrint installation guide, including a supported Pango installation.
- Create or activate a virtual environment running Python 3.10 or newer.
- Install the Python package:
python -m pip install weasyprint
Minimal conversion from a string
from weasyprint import HTML
HTML(string="""
<h1>Monthly report</h1>
<p>Generated from HTML with Python.</p>
""").write_pdf("report.pdf")
Run the script from a directory where the process can create report.pdf. The same API accepts HTML from a URL, filename, or file object. Calling write_pdf() without a destination returns PDF bytes, which is useful in a web response or object-storage upload.
Write bytes instead of a local file
from weasyprint import HTML
pdf_bytes = HTML(string="<h1>Invoice 1042</h1>").write_pdf()
with open("invoice.pdf", "wb") as output:
output.write(pdf_bytes)
Convert an HTML file and resolve relative assets
from weasyprint import HTML
HTML(filename="templates/report.html",
base_url="templates/").write_pdf("report.pdf")
base_url gives relative images, stylesheets, and fonts a known location. Without an appropriate base URL, a file that looks correct in a browser can produce a PDF with missing assets.
Provide CSS explicitly
from weasyprint import CSS, HTML
html = HTML(string="""
<!doctype html>
<html><head><meta charset="utf-8"></head>
<body><h1>Quarterly report</h1></body></html>
""")
css = CSS(string="""
@page { size: A4; margin: 18mm; }
body { font-family: sans-serif; color: #222; }
h1 { break-after: avoid; }
""")
html.write_pdf("quarterly.pdf", stylesheets=[css])
Use print-oriented CSS such as @page, margins, and break rules. Keep asset URLs accessible to the conversion process and avoid assuming that every browser-only CSS or JavaScript feature is available in a direct renderer.
Free tools Windows power users keep installed
One-click scans. No signup required.
Playwright: print a Chromium page to PDF
Playwright’s Python API launches a browser, creates or navigates a page, and calls page.pdf(). The API reference says PDF generation uses print CSS media by default. If your design is written for the screen, call page.emulate_media(media="screen") before generating the PDF.
Rank #2
Install Python and browser binaries
- Install the package:
python -m pip install playwright. - Download the browser binaries required by your deployment:
playwright install. - In a container or CI image, repeat the browser installation during image build and ensure the runtime user can execute the binaries. See the library setup and browser installation documentation.
Render an HTML string
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
page.set_content("<h1>Monthly report</h1><p>Rendered in Chromium.</p>")
page.pdf(path="report.pdf")
browser.close()
The browser is closed in the same context so the process does not leak resources. For a long-running service, create a controlled browser lifecycle and close each page after its job.
Navigate to a URL and wait for page work
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
page.goto("https://example.com", wait_until="networkidle")
page.pdf(path="example.pdf", format="A4", print_background=True)
browser.close()
Use a less aggressive readiness condition when a site keeps analytics or streaming requests open forever. For application pages, wait for a specific selector that proves the report is populated rather than relying only on a timer.
Choose screen media deliberately
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
page.set_content("<link rel='stylesheet' href='report.css'><main>Report</main>")
page.emulate_media(media="screen")
page.pdf(path="screen-styled.pdf", format="A4", print_background=True)
browser.close()
Without emulate_media, Playwright uses print media. That can intentionally hide navigation, change colors, or apply print-only break rules.
Do these 3 things before closing this tab:
1Clear out junk files and repair common Windows errors2Fix the driver behind crashes, sound loss and screen glitches3Repair Windows errors before they cause bigger problemsFonts, images, links, and page breaks
Make assets deterministic
- Package fonts and images with the application or serve them from an authenticated endpoint that the renderer can reach.
- Use an explicit base URL with WeasyPrint and wait for required resources in Playwright.
- Check the generated PDF, not just the browser preview: missing fonts can change line wrapping and push headings onto another page.
Control pagination with CSS
.invoice-line { break-inside: avoid; }
.page-break { break-before: page; }
@page { size: Letter; margin: 0.6in; }
Keep tables and signature blocks together where possible, but accept that a renderer may still move content when a block cannot fit on a page. Create test fixtures for unusually long names, large tables, and empty sections.
Security boundaries for untrusted input
Do not treat arbitrary user-supplied HTML, CSS, URLs, or scripts as safe to render. WeasyPrint’s documentation explicitly warns: “Using WeasyPrint with untrusted HTML or untrusted CSS may lead to various security problems.” The common use cases guidance should be part of your threat model.
- Allow-list templates, CSS properties, URL schemes, and remote hosts.
- Isolate conversion workers and apply CPU, memory, time, and output-size limits.
- For Playwright, disable or restrict access to internal network addresses when users can control navigation.
- Sanitize data before inserting it into HTML and never interpolate untrusted strings into executable JavaScript.
Troubleshooting common failures
WeasyPrint cannot import or start
Cause: a missing or incompatible native dependency, commonly among the text and layout libraries. Fix: install the packages listed for your OS in the current first-steps guide, verify Python and Pango versions, then reinstall the Python package inside the active virtual environment.
Images or styles are missing
Cause: relative URLs have no base location, or the worker cannot reach a remote asset. Fix: pass base_url, use stable absolute URLs, and verify file permissions and network access from the conversion process.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
Playwright reports that an executable is missing
Cause: the Python package is installed but browser binaries are not. Fix: run playwright install during setup or image build, and confirm the runtime user can read and execute the installed files.
The PDF looks different from the web page
Cause: Playwright prints with print media by default, or the direct renderer does not implement a browser-only feature. Fix: try page.emulate_media(media="screen") when screen styling is intended, replace unsupported dependencies, and maintain visual regression samples.
The page is blank or incomplete
Cause: capture occurred before data or fonts loaded. Fix: wait for a meaningful selector or application-ready signal, then inspect browser console and network errors. Avoid an arbitrary long sleep as the only readiness test.
Neither source set supplies a universal speed or fidelity winner. Measure your own templates with realistic data. WeasyPrint avoids downloading a browser, which can simplify a small worker; Playwright’s browser process adds installation and memory planning but handles browser navigation and JavaScript. ScreenshotNeo is a website screenshot API and MCP server that can return PNG, JPEG, WebP, or PDF from one GET request. It accepts cookie and consent banners like a visitor, then removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each step can be disabled. Bot checks, CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and the response identifies the result with Recommended Free Tools For a direct call, use the documented endpoint (see the ScreenshotNeo API documentation): The same request from Python: And Node.js: ScreenshotNeo also offers an MCP server with Yes. With WeasyPrint, omit the destination in The documented Python workflow requires Playwright browser binaries. Install them separately with Start with Playwright because it creates a browser page and can wait for application state. Validate the exact page and security model before production. No. Treat markup, CSS, URLs, and scripts as untrusted until sanitized and isolated; rendering itself can expose security risks. For controlled HTML, WeasyPrint’s Test long text, fonts, images, links, tables, page breaks, missing assets, and the renderer versions used in your deployment. Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Or skip the browser setup
X-Page-Verdict and X-Billed headers.curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webpimport requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = Buffer.from(await res.arrayBuffer());
require('fs').writeFileSync('shot.webp', data);take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. Every plan includes all features; 1,000 shots per month are free with no card, Starter is $5 for 3,000, and paid plans start at that $5 level. Sign up free to try it without a card.Best Value
FAQ
Can I return PDF bytes directly from a web framework?
write_pdf(), then send the returned bytes with a PDF content type and a suitable download header.Does Playwright always need Chromium?
playwright install and include them in deployment planning.Which engine should I use for JavaScript-generated content?
Is user-provided HTML safe to convert?
Frequently Asked Questions
What is the simplest Python HTML-to-PDF API?
HTML(...).write_pdf(...) is the shortest documented path.Why is my Playwright PDF using the wrong colors or layout?
page.pdf() uses print CSS media by default; call page.emulate_media(media="screen") when you need screen styles.What should I test before shipping generated PDFs?
Quick Recap

